diff --git a/libraries/stdlib/jvm/src/kotlin/collections/builders/ListBuilder.kt b/libraries/stdlib/jvm/src/kotlin/collections/builders/ListBuilder.kt index ffa1fe6e62e..dd561ca0411 100644 --- a/libraries/stdlib/jvm/src/kotlin/collections/builders/ListBuilder.kt +++ b/libraries/stdlib/jvm/src/kotlin/collections/builders/ListBuilder.kt @@ -1,10 +1,14 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package kotlin.collections.builders +import java.io.Externalizable +import java.io.InvalidObjectException +import java.io.NotSerializableException + internal class ListBuilder private constructor( private var array: Array, private var offset: Int, @@ -12,7 +16,7 @@ internal class ListBuilder private constructor( private var isReadOnly: Boolean, private val backing: ListBuilder?, private val root: ListBuilder? -) : MutableList, RandomAccess, AbstractMutableList() { +) : MutableList, RandomAccess, AbstractMutableList(), Serializable { constructor() : this(10) @@ -26,6 +30,12 @@ internal class ListBuilder private constructor( return this } + private fun writeReplace(): Any = + if (isEffectivelyReadOnly) + SerializedCollection(this, SerializedCollection.tagList) + else + throw NotSerializableException("The list cannot be serialized while it is being built.") + override val size: Int get() = length @@ -175,9 +185,12 @@ internal class ListBuilder private constructor( } private fun checkIsMutable() { - if (isReadOnly || root != null && root.isReadOnly) throw UnsupportedOperationException() + if (isEffectivelyReadOnly) throw UnsupportedOperationException() } + private val isEffectivelyReadOnly: Boolean + get() = isReadOnly || root != null && root.isReadOnly + private fun ensureExtraCapacity(n: Int) { ensureCapacity(length + n) } @@ -367,4 +380,50 @@ internal fun Array.resetAt(index: Int) { internal fun Array.resetRange(fromIndex: Int, toIndex: Int) { for (index in fromIndex until toIndex) resetAt(index) +} + +internal class SerializedCollection( + private var collection: Collection<*>, + private val tag: Int +) : Externalizable { + + constructor() : this(emptyList(), 0) // for deserialization + + override fun writeExternal(output: java.io.ObjectOutput) { + output.writeByte(tag) + output.writeInt(collection.size) + for (element in collection) { + output.writeObject(element) + } + } + + @OptIn(ExperimentalStdlibApi::class) + override fun readExternal(input: java.io.ObjectInput) { + val flags = input.readByte().toInt() + val tag = flags and 1 + val other = flags and 1.inv() + if (other != 0) { + throw InvalidObjectException("Unsupported flags value: $flags.") + } + val size = input.readInt() + if (size < 0) throw InvalidObjectException("Illegal size value: $size.") + collection = when (tag) { + tagList -> buildList(size) { + repeat(size) { add(input.readObject()) } + } + tagSet -> buildSet(size) { + repeat(size) { add(input.readObject()) } + } + else -> + throw InvalidObjectException("Unsupported collection type tag: $tag.") + } + } + + private fun readResolve(): Any = collection + + companion object { + private const val serialVersionUID: Long = 0L + const val tagList: Int = 0 + const val tagSet: Int = 1 + } } \ No newline at end of file diff --git a/libraries/stdlib/jvm/src/kotlin/collections/builders/MapBuilder.kt b/libraries/stdlib/jvm/src/kotlin/collections/builders/MapBuilder.kt index 61ef763b23e..0b0e8c5a545 100644 --- a/libraries/stdlib/jvm/src/kotlin/collections/builders/MapBuilder.kt +++ b/libraries/stdlib/jvm/src/kotlin/collections/builders/MapBuilder.kt @@ -1,10 +1,14 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package kotlin.collections.builders +import java.io.Externalizable +import java.io.InvalidObjectException +import java.io.NotSerializableException + internal class MapBuilder private constructor( private var keysArray: Array, private var valuesArray: Array?, // allocated only when actually used, always null in pure HashSet @@ -12,7 +16,7 @@ internal class MapBuilder private constructor( private var hashArray: IntArray, private var maxProbeDistance: Int, private var length: Int -) : MutableMap { +) : MutableMap, Serializable { private var hashShift: Int = computeShift(hashSize) override var size: Int = 0 @@ -22,7 +26,8 @@ internal class MapBuilder private constructor( private var valuesView: MapBuilderValues? = null private var entriesView: MapBuilderEntries? = null - private var isReadOnly: Boolean = false + internal var isReadOnly: Boolean = false + private set // ---------------------------- functions ---------------------------- @@ -42,6 +47,12 @@ internal class MapBuilder private constructor( return this } + private fun writeReplace(): Any = + if (isReadOnly) + SerializedMap(this) + else + throw NotSerializableException("The map cannot be serialized while it is being built.") + override fun isEmpty(): Boolean = size == 0 override fun containsKey(key: K): Boolean = findKey(key) >= 0 override fun containsValue(value: V): Boolean = findValue(value) >= 0 @@ -624,3 +635,42 @@ internal class MapBuilderEntries internal constructor( return super.retainAll(elements) } } + +private class SerializedMap( + private var map: Map<*, *> +) : Externalizable { + + constructor() : this(emptyMap()) // for deserialization + + override fun writeExternal(output: java.io.ObjectOutput) { + output.writeByte(0) // flags + output.writeInt(map.size) + for (entry in map) { + output.writeObject(entry.key) + output.writeObject(entry.value) + } + } + + @OptIn(ExperimentalStdlibApi::class) + override fun readExternal(input: java.io.ObjectInput) { + val flags = input.readByte().toInt() + if (flags != 0) { + throw InvalidObjectException("Unsupported flags value: $flags") + } + val size = input.readInt() + if (size < 0) throw InvalidObjectException("Illegal size value: $size.") + map = buildMap(size) { + repeat(size) { + val key = input.readObject() + val value = input.readObject() + put(key, value) + } + } + } + + private fun readResolve(): Any = map + + companion object { + private const val serialVersionUID: Long = 0L + } +} diff --git a/libraries/stdlib/jvm/src/kotlin/collections/builders/SetBuilder.kt b/libraries/stdlib/jvm/src/kotlin/collections/builders/SetBuilder.kt index 5d8298e0363..a5d8a6b7b09 100644 --- a/libraries/stdlib/jvm/src/kotlin/collections/builders/SetBuilder.kt +++ b/libraries/stdlib/jvm/src/kotlin/collections/builders/SetBuilder.kt @@ -1,13 +1,15 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package kotlin.collections.builders +import java.io.NotSerializableException + internal class SetBuilder internal constructor( private val backing: MapBuilder -) : MutableSet, AbstractMutableSet() { +) : MutableSet, AbstractMutableSet(), Serializable { constructor() : this(MapBuilder()) @@ -18,6 +20,12 @@ internal class SetBuilder internal constructor( return this } + private fun writeReplace(): Any = + if (backing.isReadOnly) + SerializedCollection(this, SerializedCollection.tagSet) + else + throw NotSerializableException("The set cannot be serialized while it is being built.") + override val size: Int get() = backing.size override fun isEmpty(): Boolean = backing.isEmpty() override fun contains(element: E): Boolean = backing.containsKey(element) diff --git a/libraries/stdlib/jvm/test/collections/CollectionJVMTest.kt b/libraries/stdlib/jvm/test/collections/CollectionJVMTest.kt index a3c93491185..965b5f5ba08 100644 --- a/libraries/stdlib/jvm/test/collections/CollectionJVMTest.kt +++ b/libraries/stdlib/jvm/test/collections/CollectionJVMTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -8,6 +8,8 @@ package test.collections import test.assertStaticAndRuntimeTypeIs import test.io.deserializeFromHex import test.io.serializeAndDeserialize +import test.io.serializeToByteArray +import java.io.NotSerializableException import java.util.* import kotlin.test.* @@ -186,7 +188,7 @@ class CollectionJVMTest { val result = serializeAndDeserialize(value) assertEquals(value, result) - assertTrue(value === result) + assertSame(value, result) } @Test fun deserializeEmptyList() = testPersistedDeserialization( @@ -205,4 +207,57 @@ class CollectionJVMTest { val actual = deserializeFromHex(hexValue) assertEquals(expected, actual) } + + @Test + fun builtListIsSerializable() { + val source = buildList { + repeat(5) { add(it.toLong()) } + add("string") + add(null) + assertFailsWith { serializeToByteArray(this@buildList) } + assertFailsWith { serializeToByteArray(this@buildList.subList(0, 2)) } + } + testCollectionBuilderSerialization(source) + testCollectionBuilderSerialization(source.subList(0, source.size - 1)) + } + + @Test + fun builtSetIsSerializable() { + val source = buildSet { + repeat(5) { add(it.toShort()) } + repeat(5) { add(it.toLong()) } + add("string") + add('c') + add(null) + assertFailsWith { serializeToByteArray(this@buildSet) } + } + testCollectionBuilderSerialization(source) + } + + @Test + fun builtMapIsSerializable() { + val source = buildMap { + repeat(5) { put(it.toShort(), it.toLong()) } + put('s', "string") + put(null, null) + assertFailsWith { serializeToByteArray(this@buildMap) } + } + testCollectionBuilderSerialization(source) + } + + + private fun testCollectionBuilderSerialization(value: Any) { + val result = serializeAndDeserialize(value) + assertEquals(value, result) + assertEquals(value.javaClass, result.javaClass) + assertReadOnly(result) + } + + private fun assertReadOnly(collection: Any) { + when (collection) { + is MutableCollection<*> -> assertFails { collection.clear() } + is MutableMap<*, *> -> assertFails { collection.clear() } + } + } + } diff --git a/libraries/stdlib/src/kotlin/collections/Collections.kt b/libraries/stdlib/src/kotlin/collections/Collections.kt index 8f4b9175960..000141f0bae 100644 --- a/libraries/stdlib/src/kotlin/collections/Collections.kt +++ b/libraries/stdlib/src/kotlin/collections/Collections.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -162,6 +162,8 @@ public inline fun MutableList(size: Int, init: (index: Int) -> T): MutableLi * The list passed as a receiver to the [builderAction] is valid only inside that function. * Using it outside of the function produces an unspecified behavior. * + * The returned list is serializable (JVM). + * * @sample samples.collections.Builders.Lists.buildListSample */ @SinceKotlin("1.3") @@ -185,6 +187,8 @@ internal expect inline fun buildListInternal(builderAction: MutableList.( * The list passed as a receiver to the [builderAction] is valid only inside that function. * Using it outside of the function produces an unspecified behavior. * + * The returned list is serializable (JVM). + * * [capacity] is used to hint the expected number of elements added in the [builderAction]. * * @throws IllegalArgumentException if the given [capacity] is negative. diff --git a/libraries/stdlib/src/kotlin/collections/Maps.kt b/libraries/stdlib/src/kotlin/collections/Maps.kt index 43ea50b16bf..f47151e333f 100644 --- a/libraries/stdlib/src/kotlin/collections/Maps.kt +++ b/libraries/stdlib/src/kotlin/collections/Maps.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -132,6 +132,8 @@ public fun linkedMapOf(vararg pairs: Pair): LinkedHashMap = p * * Entries of the map are iterated in the order they were added by the [builderAction]. * + * The returned map is serializable (JVM). + * * @sample samples.collections.Builders.Maps.buildMapSample */ @SinceKotlin("1.3") @@ -159,6 +161,8 @@ internal expect inline fun buildMapInternal(builderAction: MutableMap setOfNotNull(vararg elements: T?): Set { * * Elements of the set are iterated in the order they were added by the [builderAction]. * + * The returned set is serializable (JVM). + * * @sample samples.collections.Builders.Sets.buildSetSample */ @SinceKotlin("1.3") @@ -145,6 +147,8 @@ internal expect inline fun buildSetInternal(builderAction: MutableSet.() * * Elements of the set are iterated in the order they were added by the [builderAction]. * + * The returned set is serializable (JVM). + * * @throws IllegalArgumentException if the given [capacity] is negative. * * @sample samples.collections.Builders.Sets.buildSetSample