Skip to content

Commit bd1ee14

Browse files
fzhinkinSpace Team
authored andcommitted
KT-78739 Provide Uuid.parse*OrNull functions
^KT-78739 fixed Merge-request: KT-MR-22971 Merged-by: Filipp Zhinkin <[email protected]>
1 parent c704685 commit bd1ee14

File tree

9 files changed

+330
-55
lines changed

9 files changed

+330
-55
lines changed

libraries/stdlib/js/src/kotlin/uuid/UuidJs.kt

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,40 @@ private fun ByteArray.setIntAt(index: Int, value: Int) {
7878
}
7979

8080
// Avoid bitwise operations with Longs in JS
81-
@OptIn(ExperimentalStdlibApi::class)
8281
@ExperimentalUuidApi
8382
internal actual fun uuidParseHexDash(hexDashString: String): Uuid {
83+
return uuidParseHexDash(hexDashString) { inputString, errorDescription, errorIndex ->
84+
uuidThrowUnexpectedCharacterException(inputString, errorDescription, errorIndex)
85+
}
86+
}
87+
88+
// Avoid bitwise operations with Longs in JS
89+
@ExperimentalUuidApi
90+
internal actual fun uuidParseHexDashOrNull(hexDashString: String): Uuid? {
91+
return uuidParseHexDash(hexDashString) { _, _, _ ->
92+
return null
93+
}
94+
}
95+
96+
@ExperimentalUuidApi
97+
internal inline fun uuidParseHexDash(
98+
hexDashString: String,
99+
onError: (inputString: String, errorDescription: String, errorIndex: Int) -> Nothing
100+
): Uuid {
101+
val hexDigitExpectedMessage = "a hexadecimal digit"
102+
84103
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
85104
// 8 hex digits fit into an Int
86-
val part1 = hexDashString.hexToInt(startIndex = 0, endIndex = 8)
87-
hexDashString.checkHyphenAt(8)
88-
val part2 = hexDashString.hexToInt(startIndex = 9, endIndex = 13)
89-
hexDashString.checkHyphenAt(13)
90-
val part3 = hexDashString.hexToInt(startIndex = 14, endIndex = 18)
91-
hexDashString.checkHyphenAt(18)
92-
val part4 = hexDashString.hexToInt(startIndex = 19, endIndex = 23)
93-
hexDashString.checkHyphenAt(23)
94-
val part5a = hexDashString.hexToInt(startIndex = 24, endIndex = 28)
95-
val part5b = hexDashString.hexToInt(startIndex = 28, endIndex = 36)
105+
val part1 = hexDashString.parseHexToInt(startIndex = 0, endIndex = 8) { onError(this, hexDigitExpectedMessage, it) }
106+
hexDashString.uuidCheckHyphenAt(8, onError)
107+
val part2 = hexDashString.parseHexToInt(startIndex = 9, endIndex = 13) { onError(this, hexDigitExpectedMessage, it) }
108+
hexDashString.uuidCheckHyphenAt(13, onError)
109+
val part3 = hexDashString.parseHexToInt(startIndex = 14, endIndex = 18) { onError(this, hexDigitExpectedMessage, it) }
110+
hexDashString.uuidCheckHyphenAt(18, onError)
111+
val part4 = hexDashString.parseHexToInt(startIndex = 19, endIndex = 23) { onError(this, hexDigitExpectedMessage, it) }
112+
hexDashString.uuidCheckHyphenAt(23, onError)
113+
val part5a = hexDashString.parseHexToInt(startIndex = 24, endIndex = 28) { onError(this, hexDigitExpectedMessage, it) }
114+
val part5b = hexDashString.parseHexToInt(startIndex = 28, endIndex = 36) { onError(this, hexDigitExpectedMessage, it) }
96115

97116
@OptIn(BoxedLongApi::class) // Long constructor is intrinsified when BigInt-backed Longs are enabled.
98117
val msb = Long(
@@ -109,20 +128,37 @@ internal actual fun uuidParseHexDash(hexDashString: String): Uuid {
109128
}
110129

111130
// Avoid bitwise operations with Longs in JS
112-
@OptIn(ExperimentalStdlibApi::class)
113131
@ExperimentalUuidApi
114132
internal actual fun uuidParseHex(hexString: String): Uuid {
133+
return uuidParseHex(hexString) { inputString, errorDescription, errorIndex ->
134+
uuidThrowUnexpectedCharacterException(inputString, errorDescription, errorIndex)
135+
}
136+
}
137+
138+
// Avoid bitwise operations with Longs in JS
139+
@ExperimentalUuidApi
140+
internal actual fun uuidParseHexOrNull(hexString: String): Uuid? {
141+
return uuidParseHex(hexString) { _, _, _ ->
142+
return null
143+
}
144+
}
145+
146+
@ExperimentalUuidApi
147+
private inline fun uuidParseHex(
148+
hexString: String,
149+
onError: (inputString: String, errorDescription: String, errorIndex: Int) -> Nothing
150+
): Uuid {
115151
// 8 hex digits fit into an Int
116152
@OptIn(BoxedLongApi::class) // Long constructor is intrinsified when BigInt-backed Longs are enabled.
117153
val msb = Long(
118-
high = hexString.hexToInt(startIndex = 0, endIndex = 8),
119-
low = hexString.hexToInt(startIndex = 8, endIndex = 16)
154+
high = hexString.parseHexToInt(startIndex = 0, endIndex = 8) { onError(this, "a hexadecimal digit", it) },
155+
low = hexString.parseHexToInt(startIndex = 8, endIndex = 16) { onError(this, "a hexadecimal digit", it) }
120156
)
121157

122158
@OptIn(BoxedLongApi::class) // Long constructor is intrinsified when BigInt-backed Longs are enabled.
123159
val lsb = Long(
124-
high = hexString.hexToInt(startIndex = 16, endIndex = 24),
125-
low = hexString.hexToInt(startIndex = 24, endIndex = 32)
160+
high = hexString.parseHexToInt(startIndex = 16, endIndex = 24) { onError(this, "a hexadecimal digit", it) },
161+
low = hexString.parseHexToInt(startIndex = 24, endIndex = 32) { onError(this, "a hexadecimal digit", it) }
126162
)
127163
return Uuid.fromLongs(msb, lsb)
128164
}

libraries/stdlib/jvm/src/kotlin/uuid/UuidJVM.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,18 @@ internal actual fun ByteArray.setLongAt(index: Int, value: Long) =
6969
internal actual fun uuidParseHexDash(hexDashString: String): Uuid =
7070
uuidParseHexDashCommonImpl(hexDashString)
7171

72+
@ExperimentalUuidApi
73+
internal actual fun uuidParseHexDashOrNull(hexDashString: String): Uuid? =
74+
uuidParseHexDashOrNullCommonImpl(hexDashString)
75+
7276
@ExperimentalUuidApi
7377
internal actual fun uuidParseHex(hexString: String): Uuid =
7478
uuidParseHexCommonImpl(hexString)
7579

80+
@ExperimentalUuidApi
81+
internal actual fun uuidParseHexOrNull(hexString: String): Uuid? =
82+
uuidParseHexOrNullCommonImpl(hexString)
83+
7684
/**
7785
* Converts this [java.util.UUID] value to the corresponding [kotlin.uuid.Uuid] value.
7886
*
@@ -274,4 +282,4 @@ public fun ByteBuffer.putUuid(index: Int, uuid: Uuid): ByteBuffer = uuid.toLongs
274282
}
275283

276284
@Suppress("NOTHING_TO_INLINE")
277-
internal inline fun Long.reverseBytes(): Long = java.lang.Long.reverseBytes(this)
285+
internal inline fun Long.reverseBytes(): Long = java.lang.Long.reverseBytes(this)

libraries/stdlib/native-wasm/src/kotlin/uuid/UuidNativeWasm.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ internal actual fun ByteArray.setLongAt(index: Int, value: Long) =
2525
internal actual fun uuidParseHexDash(hexDashString: String): Uuid =
2626
uuidParseHexDashCommonImpl(hexDashString)
2727

28+
@ExperimentalUuidApi
29+
internal actual fun uuidParseHexDashOrNull(hexDashString: String): Uuid? =
30+
uuidParseHexDashOrNullCommonImpl(hexDashString)
31+
2832
@ExperimentalUuidApi
2933
internal actual fun uuidParseHex(hexString: String): Uuid =
30-
uuidParseHexCommonImpl(hexString)
34+
uuidParseHexCommonImpl(hexString)
35+
36+
@ExperimentalUuidApi
37+
internal actual fun uuidParseHexOrNull(hexString: String): Uuid? =
38+
uuidParseHexOrNullCommonImpl(hexString)

libraries/stdlib/samples/test/samples/uuid/uuid.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,18 +171,53 @@ class Uuids {
171171
assertTrue(uuid1 == uuid2)
172172
assertPrints(uuid1, "550e8400-e29b-41d4-a716-446655440000")
173173
assertPrints(uuid2, "550e8400-e29b-41d4-a716-446655440000")
174+
175+
assertFailsWith<IllegalArgumentException> { Uuid.parse("I'm not a UUID, sorry") }
176+
}
177+
178+
@Sample
179+
fun parseOrNull() {
180+
// Parsing is case-insensitive
181+
val uuid1 = Uuid.parseOrNull("550E8400-e29b-41d4-A716-446655440000") // hex-and-dash
182+
val uuid2 = Uuid.parseOrNull("550e8400E29b41D4a716446655440000") // hexadecimal
183+
184+
assertTrue(uuid1 == uuid2)
185+
assertPrints(uuid1, "550e8400-e29b-41d4-a716-446655440000")
186+
assertPrints(uuid2, "550e8400-e29b-41d4-a716-446655440000")
187+
188+
assertNull(Uuid.parseOrNull("I'm not a UUID, sorry"))
174189
}
175190

176191
@Sample
177192
fun parseHexDash() {
178193
val uuid = Uuid.parseHexDash("550E8400-e29b-41d4-A716-446655440000") // case insensitive
179194
assertPrints(uuid, "550e8400-e29b-41d4-a716-446655440000")
195+
196+
assertFailsWith<IllegalArgumentException> { Uuid.parseHexDash("550E8400/e29b/41d4/A716/446655440000") }
197+
}
198+
199+
@Sample
200+
fun parseHexDashOrNull() {
201+
val uuid = Uuid.parseHexDashOrNull("550E8400-e29b-41d4-A716-446655440000") // case insensitive
202+
assertPrints(uuid, "550e8400-e29b-41d4-a716-446655440000")
203+
204+
assertNull(Uuid.parseHexDashOrNull("550E8400/e29b/41d4/A716/446655440000"))
180205
}
181206

182207
@Sample
183208
fun parseHex() {
184209
val uuid = Uuid.parseHex("550E8400e29b41d4A716446655440000") // case insensitive
185210
assertPrints(uuid, "550e8400-e29b-41d4-a716-446655440000")
211+
212+
assertFailsWith<IllegalArgumentException> { Uuid.parseHex("~550E8400e29b41d4A716446655440000~") }
213+
}
214+
215+
@Sample
216+
fun parseHexOrNull() {
217+
val uuid = Uuid.parseHexOrNull("550E8400e29b41d4A716446655440000") // case insensitive
218+
assertPrints(uuid, "550e8400-e29b-41d4-a716-446655440000")
219+
220+
assertNull(Uuid.parseHexOrNull("~550E8400e29b41d4A716446655440000~"))
186221
}
187222

188223
@Sample
@@ -226,4 +261,4 @@ class Uuids {
226261
assertPrints(sortedUuids[1], "49d6d991-c780-4eb5-8585-5169c25af912")
227262
assertPrints(sortedUuids[2], "c0bac692-7208-4448-a8fe-3e3eb128db2a")
228263
}
229-
}
264+
}

libraries/stdlib/src/kotlin/text/HexExtensions.kt

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -546,8 +546,8 @@ private fun String.hexToByteArraySlowPath(
546546
}
547547

548548
private fun String.parseByteAt(index: Int): Byte {
549-
val high = decimalFromHexDigitAt(index)
550-
val low = decimalFromHexDigitAt(index + 1)
549+
val high = decimalFromHexDigitAt(index) { throwInvalidDigitAt(it) }
550+
val low = decimalFromHexDigitAt(index + 1) { throwInvalidDigitAt(it) }
551551
return ((high shl 4) or low).toByte()
552552
}
553553

@@ -1166,17 +1166,41 @@ private fun String.checkZeroDigits(startIndex: Int, endIndex: Int) {
11661166
}
11671167

11681168
private fun String.parseInt(startIndex: Int, endIndex: Int): Int {
1169+
return parseHexToInt(startIndex, endIndex) { index -> throwInvalidDigitAt(index) }
1170+
}
1171+
1172+
/**
1173+
* Parses [this] string's substring starting at [startIndex] and ending at [endIndex] as a hex-encoded [Int] value.
1174+
*
1175+
* If a string contains characters other than `0-9`, `a-f`, `A-F`, the function invokes [onError] callback with
1176+
* [this] as a receiver and an index at which an illegal character was found as a parameter.
1177+
*
1178+
* This function does not validate [startIndex] and [endIndex] values.
1179+
*/
1180+
internal inline fun String.parseHexToInt(startIndex: Int, endIndex: Int, onError: String.(Int) -> Nothing): Int {
11691181
var result = 0
1170-
for (i in startIndex until endIndex) {
1171-
result = (result shl 4) or decimalFromHexDigitAt(i)
1182+
for (index in startIndex until endIndex) {
1183+
result = (result shl 4) or decimalFromHexDigitAt(index) { this.onError(it) }
11721184
}
11731185
return result
11741186
}
11751187

11761188
private fun String.parseLong(startIndex: Int, endIndex: Int): Long {
1189+
return parseHexToLong(startIndex, endIndex) { index -> throwInvalidDigitAt(index) }
1190+
}
1191+
1192+
/**
1193+
* Parses [this] string's substring starting at [startIndex] and ending at [endIndex] as a hex-encoded [Long] value.
1194+
*
1195+
* If a string contains characters other than `0-9`, `a-f`, `A-F`, the function invokes [onError] callback with
1196+
* [this] as a receiver and an index at which an illegal character was found as a parameter.
1197+
*
1198+
* This function does not validate [startIndex] and [endIndex] values.
1199+
*/
1200+
internal inline fun String.parseHexToLong(startIndex: Int, endIndex: Int, onError: String.(Int) -> Nothing): Long {
11771201
var result = 0L
1178-
for (i in startIndex until endIndex) {
1179-
result = (result shl 4) or longDecimalFromHexDigitAt(i)
1202+
for (index in startIndex until endIndex) {
1203+
result = result.shl(4) or longDecimalFromHexDigitAt(index) { this.onError(it) }
11801204
}
11811205
return result
11821206
}
@@ -1193,22 +1217,20 @@ private inline fun String.checkContainsAt(index: Int, endIndex: Int, part: Strin
11931217
return index + part.length
11941218
}
11951219

1196-
@Suppress("NOTHING_TO_INLINE")
1197-
private inline fun String.decimalFromHexDigitAt(index: Int): Int {
1220+
private inline fun String.decimalFromHexDigitAt(index: Int, onError: String.(Int) -> Nothing): Int {
11981221
val code = this[index].code
11991222
if (code ushr 8 == 0 && HEX_DIGITS_TO_DECIMAL[code] >= 0) {
12001223
return HEX_DIGITS_TO_DECIMAL[code]
12011224
}
1202-
throwInvalidDigitAt(index)
1225+
onError(index)
12031226
}
12041227

1205-
@Suppress("NOTHING_TO_INLINE")
1206-
private inline fun String.longDecimalFromHexDigitAt(index: Int): Long {
1228+
private inline fun String.longDecimalFromHexDigitAt(index: Int, onError: String.(Int) -> Nothing): Long {
12071229
val code = this[index].code
12081230
if (code ushr 8 == 0 && HEX_DIGITS_TO_LONG_DECIMAL[code] >= 0) {
12091231
return HEX_DIGITS_TO_LONG_DECIMAL[code]
12101232
}
1211-
throwInvalidDigitAt(index)
1233+
onError(index)
12121234
}
12131235

12141236
private fun String.throwInvalidNumberOfDigits(startIndex: Int, endIndex: Int, specifier: String, expected: Int) {

0 commit comments

Comments
 (0)