diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt index fdbfca674..90d2a6ca8 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt @@ -43,12 +43,18 @@ internal class ByteArrayOutput { private var position: Int = 0 private fun ensureCapacity(elementsToAppend: Int) { - if (position + elementsToAppend <= array.size) { + val requiredCapacityLong = position.toLong() + elementsToAppend.toLong() + if (requiredCapacityLong > Int.MAX_VALUE) { + throw IllegalArgumentException("Required capacity exceeds maximum array size (Int.MAX_VALUE).") + } + + val requiredCapacity = requiredCapacityLong.toInt() + if (requiredCapacity <= array.size) { return } - val newArray = ByteArray((position + elementsToAppend).takeHighestOneBit() shl 1) - array.copyInto(newArray) - array = newArray + + val newCapacity = nextPowerOfTwoCapacity(requiredCapacity) + array = array.copyOf(newCapacity) } public fun toByteArray(): ByteArray { @@ -56,6 +62,29 @@ internal class ByteArrayOutput { array.copyInto(newArray, startIndex = 0, endIndex = this.position) return newArray } + + /** + * Calculates the next power-of-two capacity based on the required minimum size. + * + * This function ensures the returned value is at least as large as `minCapacity`, + * and is always a power of two, unless `minCapacity` is less than or equal to zero, + * in which case it returns 0. If the calculated power of two exceeds `Integer.MAX_VALUE`, + * it returns `Integer.MAX_VALUE`. + * + * It's useful for resizing arrays with exponential growth. + * + * @param minCapacity The minimum required capacity. + * @return A capacity value that is a power of two and ≥ minCapacity, or 0 if `minCapacity` is ≤ 0. + */ + fun nextPowerOfTwoCapacity(minCapacity: Int): Int { + if (minCapacity <= 0) return 0 + + val highestOneBit = minCapacity.takeHighestOneBit() + val maxHighestOneBit = Integer.MAX_VALUE.takeHighestOneBit() + + // Check if shifting would exceed the maximum allowed value + return if (highestOneBit < maxHighestOneBit) highestOneBit shl 1 else Integer.MAX_VALUE + } fun copyFrom(src: ByteArrayOutput) { write(src.array, count = src.position) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt new file mode 100644 index 000000000..958728cf7 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt @@ -0,0 +1,57 @@ +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.* +import kotlin.test.* + +class StreamsTest { + + @Test + fun powerOfTwoCapacity_negativeValue() { + assertEquals(0, nextPowerOfTwoCapacity(-1)) + assertEquals(0, nextPowerOfTwoCapacity(-17)) + } + + @Test + fun powerOfTwoCapacity_zeroValue() { + assertEquals(0, nextPowerOfTwoCapacity(0)) + } + + @Test + fun powerOfTwoCapacity_exactPowerOfTwo() { + assertEquals(16, nextPowerOfTwoCapacity(8)) + assertEquals(32, nextPowerOfTwoCapacity(16)) + assertEquals(64, nextPowerOfTwoCapacity(32)) + } + + @Test + fun powerOfTwoCapacity_nonPowerOfTwo() { + assertEquals(16, nextPowerOfTwoCapacity(9)) + assertEquals(64, nextPowerOfTwoCapacity(33)) + assertEquals(128, nextPowerOfTwoCapacity(65)) + } + + @Test + fun powerOfTwoCapacity_smallValues() { + assertEquals(2, nextPowerOfTwoCapacity(1)) + assertEquals(4, nextPowerOfTwoCapacity(2)) + assertEquals(4, nextPowerOfTwoCapacity(3)) + } + + @Test + fun powerOfTwoCapacity_boundaryValues() { + assertEquals(0, nextPowerOfTwoCapacity(0)) + assertEquals(2, nextPowerOfTwoCapacity(1)) + assertEquals(4, nextPowerOfTwoCapacity(3)) + assertEquals(8, nextPowerOfTwoCapacity(5)) + } + + @Test + fun powerOfTwoCapacity_largeValues() { + assertEquals(1073741824, nextPowerOfTwoCapacity(536870912)) + assertEquals(1073741824, nextPowerOfTwoCapacity(1073741823)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(1073741824)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(1073741825)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(Integer.MAX_VALUE-1)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(Integer.MAX_VALUE)) + } +} diff --git a/gradle.properties b/gradle.properties index 89fe62072..deb1585fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,5 +23,3 @@ org.gradle.kotlin.dsl.allWarningsAsErrors=true kotlin.native.distribution.type=prebuilt org.gradle.jvmargs="-XX:+HeapDumpOnOutOfMemoryError" - -org.jetbrains.dokka.experimental.tryK2=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22b2552b8..132a67b61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.1.20" kover = "0.8.2" -dokka = "2.0.0-Beta" +dokka = "2.0.0" knit = "0.5.0" bcv = "0.16.2" animalsniffer = "1.7.1"