From 470c0efb26798c3e095e38482f819d8d8221d079 Mon Sep 17 00:00:00 2001 From: ilya-g Date: Wed, 15 May 2019 13:35:06 +0300 Subject: [PATCH] String <-> CharArray, ByteArray conversions (#2973) --- backend.native/tests/build.gradle | 4 +- backend.native/tests/runtime/text/utf8.kt | 16 ++- .../text/StringEncodingTestNative.kt | 11 ++ backend.native/tests/stdlib_external/utils.kt | 4 + gradle.properties | 6 +- runtime/src/main/cpp/Exceptions.h | 4 +- runtime/src/main/cpp/KString.cpp | 24 ++-- runtime/src/main/cpp/utf8/core.h | 45 ++++--- runtime/src/main/cpp/utf8/with_replacement.h | 6 +- runtime/src/main/kotlin/kotlin/Exceptions.kt | 1 + runtime/src/main/kotlin/kotlin/native/Text.kt | 51 ++++++-- .../kotlin/native/internal/RuntimeUtils.kt | 5 +- .../kotlin/text/CharacterCodingException.kt | 15 +++ .../src/main/kotlin/kotlin/text/Strings.kt | 114 +++++++++++++++++- 14 files changed, 236 insertions(+), 70 deletions(-) create mode 100644 backend.native/tests/stdlib_external/text/StringEncodingTestNative.kt create mode 100644 runtime/src/main/kotlin/kotlin/text/CharacterCodingException.kt diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index 3324a581788..0a5cc29462e 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -3472,10 +3472,10 @@ task coverage_basic_library(type: RunStandaloneKonanTest) { task buildKonanStdlibTests(type: BuildKonanTest) { outputSourceSetName = "testOutputStdlib" - flags = [ "-Xmulti-platform", "-friend-modules", project.rootProject.file("${project.properties['konan.home']}/klib/common/stdlib") ] + flags = [ "-Xmulti-platform", '-Xuse-experimental=kotlin.ExperimentalStdlibApi', "-friend-modules", project.rootProject.file("${project.properties['konan.home']}/klib/common/stdlib") ] def testSources = externalStdlibTestsDir.absolutePath compileList = [ "$testSources/stdlib/test", "$testSources/stdlib/common", - "stdlib_external/utils.kt", "stdlib_external/jsCollectionFactoriesActuals.kt" ] + "stdlib_external/utils.kt", "stdlib_external/text/StringEncodingTestNative.kt", "stdlib_external/jsCollectionFactoriesActuals.kt" ] excludeList = [ "$testSources/stdlib/test/internalAnnotations.kt" ] } diff --git a/backend.native/tests/runtime/text/utf8.kt b/backend.native/tests/runtime/text/utf8.kt index 0428f4a26b2..7a349e25628 100644 --- a/backend.native/tests/runtime/text/utf8.kt +++ b/backend.native/tests/runtime/text/utf8.kt @@ -66,7 +66,7 @@ fun checkThrows(e: KClass, string: String, action: () -> Unit) { assertTrue(e.isInstance(exception),""" Wrong exception was thrown for string: $string Expected: ${e.qualifiedName} - Actual: ${exception::class.qualifiedName} + Actual: ${exception::class.qualifiedName}: $exception} """.trimIndent()) } @@ -197,7 +197,7 @@ fun test16to8CustomBorders() { checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8(-1, 4)} checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8(5, 10)} checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8(2, 10)} - checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8(3, -2)} + checkThrows(IllegalArgumentException::class, "Hello") { "Hello".toUtf8(3, -2)} // Test manual conversion with an exception if an input is invalid and custom borders. @@ -228,7 +228,7 @@ fun test16to8CustomBorders() { checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8OrThrow(-1, 4)} checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8OrThrow(5, 10)} checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8OrThrow(2, 10)} - checkThrows(IndexOutOfBoundsException::class, "Hello") { "Hello".toUtf8OrThrow(3, -2)} + checkThrows(IllegalArgumentException::class, "Hello") { "Hello".toUtf8OrThrow(3, -2)} } fun testPrint() { @@ -294,8 +294,6 @@ fun test8to16CustomBorders() { intArrayOf('H'.toInt(), 'e'.toInt(), 'l'.toInt(), 'l'.toInt(), 'o'.toInt()), 3, 2) checkUtf8to16Replacing("", intArrayOf('H'.toInt(), 'e'.toInt(), 'l'.toInt(), 'l'.toInt(), 'o'.toInt()), 0, 0) - checkUtf8to16Replacing("", - intArrayOf('H'.toInt(), 'e'.toInt(), 'l'.toInt(), 'l'.toInt(), 'o'.toInt()), 10, 0) checkUtf8to16Replacing("\uD800\uDC00\uD800\uDC00", intArrayOf(-16, -112, -128, -128, -16, -112, -128, -128, -16, -112, -128, -128, -16, -112, -128, -128), @@ -323,7 +321,8 @@ fun test8to16CustomBorders() { checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8(-1, 4)} checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8(5, 10)} checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8(2, 10)} - checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8(3, -2)} + checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8(10, 0)} + checkThrows(IllegalArgumentException::class, "Hello") { helloArray.stringFromUtf8(3, -2)} // Conversion with throwing checkUtf8to16Throwing("He", @@ -334,8 +333,6 @@ fun test8to16CustomBorders() { intArrayOf('H'.toInt(), 'e'.toInt(), 'l'.toInt(), 'l'.toInt(), 'o'.toInt()), 3, 2) checkUtf8to16Throwing("", intArrayOf('H'.toInt(), 'e'.toInt(), 'l'.toInt(), 'l'.toInt(), 'o'.toInt()), 0, 0) - checkUtf8to16Throwing("", - intArrayOf('H'.toInt(), 'e'.toInt(), 'l'.toInt(), 'l'.toInt(), 'o'.toInt()), 10, 0) checkUtf8to16Throwing("\uD800\uDC00\uD800\uDC00", intArrayOf(-16, -112, -128, -128, -16, -112, -128, -128, -16, -112, -128, -128, -16, -112, -128, -128), @@ -361,7 +358,8 @@ fun test8to16CustomBorders() { checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8OrThrow(-1, 4)} checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8OrThrow(5, 10)} checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8OrThrow(2, 10)} - checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8OrThrow(3, -2)} + checkThrows(IndexOutOfBoundsException::class, "Hello") { helloArray.stringFromUtf8OrThrow(10, 0)} + checkThrows(IllegalArgumentException::class, "Hello") { helloArray.stringFromUtf8OrThrow(3, -2)} } // endregion diff --git a/backend.native/tests/stdlib_external/text/StringEncodingTestNative.kt b/backend.native/tests/stdlib_external/text/StringEncodingTestNative.kt new file mode 100644 index 00000000000..e4148285d97 --- /dev/null +++ b/backend.native/tests/stdlib_external/text/StringEncodingTestNative.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +package test.text + + +internal actual val surrogateCodePointDecoding: String = "\uFFFD".repeat(3) + +internal actual val surrogateCharEncoding: ByteArray = byteArrayOf(0xEF.toByte(), 0xBF.toByte(), 0xBD.toByte()) diff --git a/backend.native/tests/stdlib_external/utils.kt b/backend.native/tests/stdlib_external/utils.kt index 5aeca907ca8..e2cd58908d9 100644 --- a/backend.native/tests/stdlib_external/utils.kt +++ b/backend.native/tests/stdlib_external/utils.kt @@ -17,3 +17,7 @@ public actual fun assertTypeEquals(expected: Any?, actual: Any?) { } internal actual fun String.removeLeadingPlusOnJava6(): String = this + +internal actual inline fun testOnNonJvm6And7(f: () -> Unit) { + f() +} diff --git a/gradle.properties b/gradle.properties index c39d73e6e81..46d7160ba59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,9 +18,9 @@ buildKotlinVersion=1.3.40-dev-2251 buildKotlinCompilerRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_dev_Compiler),number:1.3.40-dev-2251,pinned:true/artifacts/content/maven remoteRoot=konan_tests -kotlinCompilerRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_dev_Compiler),number:1.3.40-dev-2423,branch:default:true,pinned:true/artifacts/content/maven -kotlinVersion=1.3.40-dev-2423 -testKotlinVersion=1.3.40-dev-2423 +kotlinCompilerRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_dev_Compiler),number:1.3.50-dev-44,branch:default:true,pinned:true/artifacts/content/maven +kotlinVersion=1.3.50-dev-44 +testKotlinVersion=1.3.50-dev-44 # See https://teamcity.jetbrains.com/project.html?projectId=Kotlin_KotlinNativeShared&tab=projectOverview sharedRepo=https://dl.bintray.com/jetbrains/kotlin-native-dependencies sharedVersion=1.0-dev-57 diff --git a/runtime/src/main/cpp/Exceptions.h b/runtime/src/main/cpp/Exceptions.h index 6e8d3dc3716..ca3bb51afea 100644 --- a/runtime/src/main/cpp/Exceptions.h +++ b/runtime/src/main/cpp/Exceptions.h @@ -52,8 +52,8 @@ void RUNTIME_NORETURN ThrowNumberFormatException(); void RUNTIME_NORETURN ThrowOutOfMemoryError(); // Throws not implemented error. void RUNTIME_NORETURN ThrowNotImplementedError(); -// Throws illegal character conversion exception (used in UTF8/UTF16 conversions). -void RUNTIME_NORETURN ThrowIllegalCharacterConversionException(); +// Throws character coding exception (used in UTF8/UTF16 conversions). +void RUNTIME_NORETURN ThrowCharacterCodingException(); void RUNTIME_NORETURN ThrowIllegalArgumentException(); void RUNTIME_NORETURN ThrowIllegalStateException(); void RUNTIME_NORETURN ThrowInvalidMutabilityException(KConstRef where); diff --git a/runtime/src/main/cpp/KString.cpp b/runtime/src/main/cpp/KString.cpp index 56a7b06c45a..c7425a7a22b 100644 --- a/runtime/src/main/cpp/KString.cpp +++ b/runtime/src/main/cpp/KString.cpp @@ -35,7 +35,7 @@ typedef KStdStringInserter utf16to8(const KChar*,const KChar*, KStdStringInserte KStdStringInserter utf16toUtf8OrThrow(const KChar* start, const KChar* end, KStdStringInserter result) { TRY_CATCH(result = utf8::utf16to8(start, end, result), result = utf8::unchecked::utf16to8(start, end, result), - ThrowIllegalCharacterConversionException()); + ThrowCharacterCodingException()); return result; } @@ -67,7 +67,7 @@ OBJ_GETTER(utf8ToUtf16OrThrow, const char* rawString, size_t rawStringLength) { uint32_t charCount; TRY_CATCH(charCount = utf8::utf16_length(rawString, end), charCount = utf8::unchecked::utf16_length(rawString, end), - ThrowIllegalCharacterConversionException()); + ThrowCharacterCodingException()); RETURN_RESULT_OF(utf8ToUtf16Impl, rawString, end, charCount); } @@ -787,11 +787,7 @@ OBJ_GETTER(Kotlin_ByteArray_stringFromUtf8OrThrow, KConstRef thiz, KInt start, K RETURN_RESULT_OF0(TheEmptyString); } const char* rawString = byteArrayAsCString(thiz, start, size); - KInt realSize = 0; - while (rawString[realSize] != 0 && realSize < size) { - realSize++; - } - RETURN_RESULT_OF(utf8ToUtf16OrThrow, rawString, realSize); + RETURN_RESULT_OF(utf8ToUtf16OrThrow, rawString, size); } OBJ_GETTER(Kotlin_ByteArray_stringFromUtf8, KConstRef thiz, KInt start, KInt size) { @@ -799,11 +795,7 @@ OBJ_GETTER(Kotlin_ByteArray_stringFromUtf8, KConstRef thiz, KInt start, KInt siz RETURN_RESULT_OF0(TheEmptyString); } const char* rawString = byteArrayAsCString(thiz, start, size); - KInt realSize = 0; - while (rawString[realSize] != 0 && realSize < size) { - realSize++; - } - RETURN_RESULT_OF(utf8ToUtf16, rawString, realSize); + RETURN_RESULT_OF(utf8ToUtf16, rawString, size); } OBJ_GETTER(Kotlin_String_toUtf8, KString thiz, KInt start, KInt size) { @@ -833,12 +825,12 @@ OBJ_GETTER(Kotlin_String_fromCharArray, KConstRef thiz, KInt start, KInt size) { RETURN_OBJ(result->obj()); } -OBJ_GETTER(Kotlin_String_toCharArray, KString string) { +OBJ_GETTER(Kotlin_String_toCharArray, KString string, KInt start, KInt size) { ArrayHeader* result = AllocArrayInstance( - theCharArrayTypeInfo, string->count_, OBJ_RESULT)->array(); + theCharArrayTypeInfo, size, OBJ_RESULT)->array(); memcpy(CharArrayAddressOfElementAt(result, 0), - CharArrayAddressOfElementAt(string, 0), - string->count_ * sizeof(KChar)); + CharArrayAddressOfElementAt(string, start), + size * sizeof(KChar)); RETURN_OBJ(result->obj()); } diff --git a/runtime/src/main/cpp/utf8/core.h b/runtime/src/main/cpp/utf8/core.h index cd4c50f32c5..f27b3c494f1 100644 --- a/runtime/src/main/cpp/utf8/core.h +++ b/runtime/src/main/cpp/utf8/core.h @@ -89,10 +89,16 @@ namespace internal return (cp >= LEAD_SURROGATE_MIN && cp <= TRAIL_SURROGATE_MAX); } + template + inline bool is_out_of_unicode_domain(u32 cp) + { + return cp > CODE_POINT_MAX; + } + template inline bool is_code_point_valid(u32 cp) { - return (cp <= CODE_POINT_MAX && !utf8::internal::is_surrogate(cp)); + return (!utf8::internal::is_out_of_unicode_domain(cp) && !utf8::internal::is_surrogate(cp)); } template @@ -146,7 +152,9 @@ namespace internal return UTF8_OK; } - #define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) {utf_error ret = increase_safely(IT, END); if (ret != UTF8_OK) return ret;} + #define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) {utf_error ret = increase_safely(IT, END); if (ret != UTF8_OK) return ret;} + + #define UTF8_CPP_RETURN_ON_OVERLONG_SEQUENCE(CP, LENGTH) {if (utf8::internal::is_overlong_sequence(CP, LENGTH)) return OVERLONG_SEQUENCE;} /// get_sequence_x functions decode utf-8 sequences of the length x template @@ -172,6 +180,8 @@ namespace internal code_point = ((code_point << 6) & 0x7ff) + ((*it) & 0x3f); + UTF8_CPP_RETURN_ON_OVERLONG_SEQUENCE(code_point, 2) + return UTF8_OK; } @@ -187,6 +197,11 @@ namespace internal code_point = ((code_point << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); + if (utf8::internal::is_surrogate(code_point)) + return INVALID_CODE_POINT; + + UTF8_CPP_RETURN_ON_OVERLONG_SEQUENCE(code_point, 3) + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point += (*it) & 0x3f; @@ -206,6 +221,11 @@ namespace internal code_point = ((code_point << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); + if (utf8::internal::is_out_of_unicode_domain(code_point)) + return INVALID_CODE_POINT; + + UTF8_CPP_RETURN_ON_OVERLONG_SEQUENCE(code_point, 4) + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point += (utf8::internal::mask8(*it) << 6) & 0xfff; @@ -251,23 +271,14 @@ namespace internal } if (err == UTF8_OK) { - // Decoding succeeded. Now, security checks... - if (utf8::internal::is_code_point_valid(cp)) { - if (!utf8::internal::is_overlong_sequence(cp, length)){ - // Passed! Return here. - code_point = cp; - ++it; - return UTF8_OK; - } - else - err = OVERLONG_SEQUENCE; - } - else - err = INVALID_CODE_POINT; + // Decoding succeeded. + code_point = cp; + ++it; + } else { + // Failure branch - restore the original value of the iterator + it = original_it; } - // Failure branch - restore the original value of the iterator - it = original_it; return err; } diff --git a/runtime/src/main/cpp/utf8/with_replacement.h b/runtime/src/main/cpp/utf8/with_replacement.h index 4807006c39d..6347677e31e 100644 --- a/runtime/src/main/cpp/utf8/with_replacement.h +++ b/runtime/src/main/cpp/utf8/with_replacement.h @@ -37,13 +37,13 @@ uint32_t next(octet_iterator &it, const octet_iterator end, uint32_t replacement case internal::UTF8_OK : return cp; case internal::INVALID_LEAD : + case internal::INVALID_CODE_POINT : + case internal::OVERLONG_SEQUENCE : it++; return replacement; case internal::NOT_ENOUGH_ROOM : case internal::INCOMPLETE_SEQUENCE : - case internal::OVERLONG_SEQUENCE : - case internal::INVALID_CODE_POINT : - // The whole invalid sequence is replaced with one replacement codepoint. + // The whole incomplete sequence is replaced with one replacement codepoint. for (it++; it < end && utf8::internal::is_trail(*it); it++); return replacement; } diff --git a/runtime/src/main/kotlin/kotlin/Exceptions.kt b/runtime/src/main/kotlin/kotlin/Exceptions.kt index 84a2fef0d01..cf4115e1a83 100644 --- a/runtime/src/main/kotlin/kotlin/Exceptions.kt +++ b/runtime/src/main/kotlin/kotlin/Exceptions.kt @@ -166,6 +166,7 @@ public actual open class NumberFormatException : IllegalArgumentException { actual constructor(message: String?) : super(message) } +// TODO: Deprecate and replace with CharacterCodingException public open class IllegalCharacterConversionException : IllegalArgumentException { constructor(): super() diff --git a/runtime/src/main/kotlin/kotlin/native/Text.kt b/runtime/src/main/kotlin/kotlin/native/Text.kt index f9e80eed173..8745897c71a 100644 --- a/runtime/src/main/kotlin/kotlin/native/Text.kt +++ b/runtime/src/main/kotlin/kotlin/native/Text.kt @@ -8,40 +8,67 @@ package kotlin.native /** * Converts an UTF-8 array into a [String]. Replaces invalid input sequences with a default character. */ -public fun ByteArray.stringFromUtf8(start: Int = 0, size: Int = this.size) : String = - stringFromUtf8Impl(start, size) +public fun ByteArray.stringFromUtf8(start: Int = 0, size: Int = this.size) : String { + checkBoundsIndexes(start, start + size, this.size) + return stringFromUtf8Impl(start, size) +} @SymbolName("Kotlin_ByteArray_stringFromUtf8") -private external fun ByteArray.stringFromUtf8Impl(start: Int, size: Int) : String +internal external fun ByteArray.stringFromUtf8Impl(start: Int, size: Int) : String /** * Converts an UTF-8 array into a [String]. * @throws [IllegalCharacterConversionException] if the input is invalid. */ -public fun ByteArray.stringFromUtf8OrThrow(start: Int = 0, size: Int = this.size) : String = - stringFromUtf8OrThrowImpl(start, size) +@UseExperimental(ExperimentalStdlibApi::class) +public fun ByteArray.stringFromUtf8OrThrow(start: Int = 0, size: Int = this.size) : String { + checkBoundsIndexes(start, start + size, this.size) + try { + return stringFromUtf8OrThrowImpl(start, size) + } catch (e: CharacterCodingException) { + throw IllegalCharacterConversionException() + } +} @SymbolName("Kotlin_ByteArray_stringFromUtf8OrThrow") -private external fun ByteArray.stringFromUtf8OrThrowImpl(start: Int, size: Int) : String +internal external fun ByteArray.stringFromUtf8OrThrowImpl(start: Int, size: Int) : String /** * Converts a [String] into an UTF-8 array. Replaces invalid input sequences with a default character. */ -public fun String.toUtf8(start: Int = 0, size: Int = this.length) : ByteArray = - toUtf8Impl(start, size) +public fun String.toUtf8(start: Int = 0, size: Int = this.length) : ByteArray { + checkBoundsIndexes(start, start + size, this.length) + return toUtf8Impl(start, size) +} @SymbolName("Kotlin_String_toUtf8") -private external fun String.toUtf8Impl(start: Int, size: Int) : ByteArray +internal external fun String.toUtf8Impl(start: Int, size: Int) : ByteArray /** * Converts a [String] into an UTF-8 array. * @throws [IllegalCharacterConversionException] if the input is invalid. */ -public fun String.toUtf8OrThrow(start: Int = 0, size: Int = this.length) : ByteArray = - toUtf8OrThrowImpl(start, size) +@UseExperimental(ExperimentalStdlibApi::class) +public fun String.toUtf8OrThrow(start: Int = 0, size: Int = this.length) : ByteArray { + checkBoundsIndexes(start, start + size, this.length) + try { + return toUtf8OrThrowImpl(start, size) + } catch (e: CharacterCodingException) { + throw IllegalCharacterConversionException() + } +} + +internal fun checkBoundsIndexes(startIndex: Int, endIndex: Int, size: Int) { + if (startIndex < 0 || endIndex > size) { + throw IndexOutOfBoundsException("startIndex: $startIndex, endIndex: $endIndex, size: $size") + } + if (startIndex > endIndex) { + throw IllegalArgumentException("startIndex: $startIndex > endIndex: $endIndex") + } +} @SymbolName("Kotlin_String_toUtf8OrThrow") -private external fun String.toUtf8OrThrowImpl(start: Int, size: Int) : ByteArray +internal external fun String.toUtf8OrThrowImpl(start: Int, size: Int) : ByteArray @SymbolName("Kotlin_String_fromCharArray") internal external fun fromCharArray(array: CharArray, start: Int, size: Int) : String diff --git a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt index fe508587f84..1b3977571fb 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt @@ -70,8 +70,9 @@ internal fun ThrowNotImplementedError(): Nothing { } @ExportForCppRuntime -internal fun ThrowIllegalCharacterConversionException(): Nothing { - throw IllegalCharacterConversionException() +internal fun ThrowCharacterCodingException(): Nothing { + @UseExperimental(ExperimentalStdlibApi::class) + throw CharacterCodingException() } @ExportForCppRuntime diff --git a/runtime/src/main/kotlin/kotlin/text/CharacterCodingException.kt b/runtime/src/main/kotlin/kotlin/text/CharacterCodingException.kt new file mode 100644 index 00000000000..72258dccb7e --- /dev/null +++ b/runtime/src/main/kotlin/kotlin/text/CharacterCodingException.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +package kotlin.text + +/** + * The exception thrown when a character encoding or decoding error occurs. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual open class CharacterCodingException(message: String?) : Exception(message) { + actual constructor() : this(null) +} diff --git a/runtime/src/main/kotlin/kotlin/text/Strings.kt b/runtime/src/main/kotlin/kotlin/text/Strings.kt index 938dab31158..f9b3d852e09 100644 --- a/runtime/src/main/kotlin/kotlin/text/Strings.kt +++ b/runtime/src/main/kotlin/kotlin/text/Strings.kt @@ -175,10 +175,12 @@ public actual external fun String.toUpperCase(): String public actual external fun String.toLowerCase(): String /** - * Returns a new character array containing the characters from this string. + * Returns a [CharArray] containing characters of this string. */ +public actual fun String.toCharArray(): CharArray = toCharArray(this, 0, length) + @SymbolName("Kotlin_String_toCharArray") -public external fun String.toCharArray(): CharArray +private external fun toCharArray(string: String, start: Int, size: Int): CharArray /** * Returns a copy of this string having its first letter uppercased, or the original string, @@ -228,7 +230,111 @@ public actual fun String(chars: CharArray): String = fromCharArray(chars, 0, cha * @throws IndexOutOfBoundsException if either [offset] or [length] are less than zero * or `offset + length` is out of [chars] array bounds. */ -public actual fun String(chars: CharArray, offset: Int, length: Int): String = fromCharArray(chars, offset, length) +public actual fun String(chars: CharArray, offset: Int, length: Int): String { + if (offset < 0 || length < 0 || offset + length > chars.size) + throw ArrayIndexOutOfBoundsException() + + return fromCharArray(chars, offset, length) +} + +/** + * Concatenates characters in this [CharArray] into a String. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual fun CharArray.concatToString(): String = fromCharArray(this, 0, size) + +/** + * Concatenates characters in this [CharArray] or its subrange into a String. + * + * @param startIndex the beginning (inclusive) of the subrange of characters, 0 by default. + * @param endIndex the end (exclusive) of the subrange of characters, size of this array by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual fun CharArray.concatToString(startIndex: Int, endIndex: Int): String { + checkBoundsIndexes(startIndex, endIndex, size) + return fromCharArray(this, startIndex, endIndex - startIndex) +} + +/** + * Returns a [CharArray] containing characters of this string or its substring. + * + * @param startIndex the beginning (inclusive) of the substring, 0 by default. + * @param endIndex the end (exclusive) of the substring, length of this string by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual fun String.toCharArray(startIndex: Int, endIndex: Int): CharArray { + checkBoundsIndexes(startIndex, endIndex, length) + return toCharArray(this, startIndex, endIndex - startIndex) +} + +/** + * Decodes a string from the bytes in UTF-8 encoding in this array. + * + * Malformed byte sequences are replaced by the replacement char `\uFFFD`. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual fun ByteArray.decodeToString(): String = stringFromUtf8Impl(0, size) + +/** + * Decodes a string from the bytes in UTF-8 encoding in this array or its subrange. + * + * @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to decode, size of this array by default. + * @param throwOnInvalidSequence specifies whether to throw an exception on malformed byte sequence or replace it by the replacement char `\uFFFD`. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + * @throws CharacterCodingException if the byte array contains malformed UTF-8 byte sequence and [throwOnInvalidSequence] is true. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual fun ByteArray.decodeToString(startIndex: Int, endIndex: Int, throwOnInvalidSequence: Boolean): String { + checkBoundsIndexes(startIndex, endIndex, size) + return if (throwOnInvalidSequence) + stringFromUtf8OrThrowImpl(startIndex, endIndex - startIndex) + else + stringFromUtf8Impl(startIndex, endIndex - startIndex) +} + +/** + * Encodes this string to an array of bytes in UTF-8 encoding. + * + * Any malformed char sequence is replaced by the replacement byte sequence. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual fun String.encodeToByteArray(): ByteArray = toUtf8Impl(0, length) + +/** + * Encodes this string or its substring to an array of bytes in UTF-8 encoding. + * + * @param startIndex the beginning (inclusive) of the substring to encode, 0 by default. + * @param endIndex the end (exclusive) of the substring to encode, length of this string by default. + * @param throwOnInvalidSequence specifies whether to throw an exception on malformed char sequence or replace. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + * @throws CharacterCodingException if this string contains malformed char sequence and [throwOnInvalidSequence] is true. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public actual fun String.encodeToByteArray(startIndex: Int, endIndex: Int, throwOnInvalidSequence: Boolean): ByteArray { + checkBoundsIndexes(startIndex, endIndex, length) + return if (throwOnInvalidSequence) + toUtf8OrThrowImpl(startIndex, endIndex - startIndex) + else + toUtf8Impl(startIndex, endIndex - startIndex) +} @SymbolName("Kotlin_String_compareToIgnoreCase") internal external fun compareToIgnoreCase(thiz: String, other: String): Int @@ -241,4 +347,4 @@ public actual fun String.compareTo(other: String, ignoreCase: Boolean): Int { private val STRING_CASE_INSENSITIVE_ORDER = Comparator { a, b -> a.compareTo(b, ignoreCase = true) } public actual val String.Companion.CASE_INSENSITIVE_ORDER: Comparator - get() = STRING_CASE_INSENSITIVE_ORDER \ No newline at end of file + get() = STRING_CASE_INSENSITIVE_ORDER