Skip to content

Commit

Permalink
Get rid of regexes
Browse files Browse the repository at this point in the history
  • Loading branch information
trema96 committed Feb 24, 2025
1 parent 278b31f commit 47e7f6d
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
package com.icure.cardinal.sdk.api.impl

import com.icure.kryptom.crypto.AesAlgorithm
import com.icure.kryptom.crypto.AesKey
import com.icure.kryptom.crypto.CryptoService
import com.icure.kryptom.crypto.PrivateRsaKey
import com.icure.kryptom.crypto.RsaAlgorithm
import com.icure.kryptom.utils.hexToByteArray
import com.icure.kryptom.utils.toHexString
import com.icure.cardinal.sdk.api.DataOwnerApi
import com.icure.cardinal.sdk.api.ShamirKeysManagerApi
import com.icure.cardinal.sdk.crypto.ExchangeDataManager
Expand All @@ -19,8 +12,16 @@ import com.icure.cardinal.sdk.model.base.CryptoActor
import com.icure.cardinal.sdk.model.extensions.asStub
import com.icure.cardinal.sdk.model.specializations.HexString
import com.icure.cardinal.sdk.model.specializations.KeypairFingerprintV1String
import com.icure.utils.InternalIcureApi
import com.icure.cardinal.sdk.utils.ensure
import com.icure.cardinal.sdk.utils.isValidHex
import com.icure.kryptom.crypto.AesAlgorithm
import com.icure.kryptom.crypto.AesKey
import com.icure.kryptom.crypto.CryptoService
import com.icure.kryptom.crypto.PrivateRsaKey
import com.icure.kryptom.crypto.RsaAlgorithm
import com.icure.kryptom.utils.hexToByteArray
import com.icure.kryptom.utils.toHexString
import com.icure.utils.InternalIcureApi

@InternalIcureApi
class ShamirKeysManagerApiImpl(
Expand Down Expand Up @@ -105,7 +106,7 @@ class ShamirKeysManagerApiImpl(
val shares = shamir.share(keyPkcs8, request.notariesIds.size, request.minShares)
val paddedShares = shares.map { "f${it.v}" }
for (share in paddedShares) {
ensure(share.matches(Regex("^(?:[0-9a-f][0-9a-f])+$"))) {
ensure(share.isValidHex()) {
"Unexpected result of shamir split: padded shares should be a valid hex value"
}
ensure(share == hexToByteArray(share).toHexString()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,7 @@ interface JsonEncryptionService {
}
}
encryptedFields.forEach { currEncryptedField ->
val (currFieldName, currFieldSeparator) = requireNotNull(
ENCRYPTED_FIELD_MANIFEST_REGEX.find(
currEncryptedField
)
) {
"Invalid encrypted field $path$currEncryptedField"
}.groupValues.let { groups ->
groups[1] to groups[2].takeIf { it.isNotEmpty() }
}
val (currFieldName, currFieldSeparator) = parseCurrentFieldAndSeparator(path, currEncryptedField)
when (currFieldSeparator) {
null -> {
if (currFieldName in topLevelFields) throw IllegalArgumentException("Duplicate encrypted field $path$currFieldName")
Expand Down Expand Up @@ -215,8 +207,37 @@ interface JsonEncryptionService {
)
}

@Suppress("RegExpRedundantEscape") // Suppressed because in node is not redundant
private val ENCRYPTED_FIELD_MANIFEST_REGEX =
Regex("^([_a-zA-Z][_a-zA-Z0-9]*|\\*)(?:(\\.\\*\\.|\\[\\]\\.|\\.)(?:[_a-zA-Z].*|\\*|\\[.*\\]))?$")
private val LOWER_RANGE = 'a' .. 'z'
private val UPPER_RANGE = 'A' .. 'Z'
private val DIGIT_RANGE = '0' .. '9'
private fun parseCurrentFieldAndSeparator(
path: String,
currEncryptedField: String
): Pair<String, String?> {
val fieldName = if (currEncryptedField.first() == '*') {
"*"
} else {
require(currEncryptedField.first().let { it in LOWER_RANGE || it in UPPER_RANGE || it == '_'}) {
"Invalid encrypted field $path$currEncryptedField - $currEncryptedField must start with a valid identifier"
}
currEncryptedField.takeWhile { it in LOWER_RANGE || it in UPPER_RANGE || it == '_' || it in DIGIT_RANGE }
}
if (fieldName.length == currEncryptedField.length) return Pair(currEncryptedField, null)
val currWithoutFieldName = currEncryptedField.substring(fieldName.length)
val separator = when {
currWithoutFieldName.startsWith(".*.") -> ".*."
currWithoutFieldName.startsWith("[].") -> "[]."
currWithoutFieldName.startsWith(".") -> "."
else -> throw IllegalArgumentException("Invalid encrypted field $path$currEncryptedField - $currWithoutFieldName contains an invalid separator")
}
require(
(currWithoutFieldName.length > separator.length && currWithoutFieldName[separator.length].let {
it in LOWER_RANGE || it in UPPER_RANGE || it == '_' || it == '['
}) || (currWithoutFieldName.length == separator.length + 1 && currWithoutFieldName.last() == '*')
) {
"Invalid encrypted field $path$currEncryptedField - Invalid followup to separator ${currWithoutFieldName.substring(separator.length)}"
}
return Pair(fieldName, separator)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.icure.cardinal.sdk.model.specializations
import com.icure.cardinal.sdk.crypto.entities.EntityWithEncryptionMetadataTypeName
import com.icure.cardinal.sdk.utils.base64Encode
import com.icure.cardinal.sdk.utils.concat
import com.icure.cardinal.sdk.utils.isValidHex
import com.icure.kryptom.crypto.CryptoService
import com.icure.kryptom.utils.hexToByteArray
import com.icure.kryptom.utils.toHexString
Expand Down Expand Up @@ -106,11 +107,12 @@ value class SpkiHexString(
) {
companion object {
internal const val TRAILING_CONSTANT = "0203010001"
internal val pattern = Regex("^(?:[0-9a-f][0-9a-f]){256,}$TRAILING_CONSTANT\$")
internal fun validSpkiHex(s: String) =
s.length >= 522 && s.endsWith(TRAILING_CONSTANT) && s.isValidHex()
}

init {
require(pattern.matches(s)) { "Invalid spki hex string: $s" }
require(validSpkiHex(s)) { "Invalid spki hex string: $s" }
}

fun fingerprintV1(): KeypairFingerprintV1String {
Expand All @@ -135,7 +137,9 @@ value class AesExchangeKeyEntryKeyString(
}

init {
require(s.startsWith(FAKE_PUB_KEY_PREFIX) || SpkiHexString.pattern.matches(s)) { "Invalid aes exchange key entry: $s" }
require(s.startsWith(FAKE_PUB_KEY_PREFIX) || SpkiHexString.validSpkiHex(s)) {
"Invalid aes exchange key entry: $s"
}
}

fun asPublicKey(): SpkiHexString? = if (s.startsWith(FAKE_PUB_KEY_PREFIX)) null else SpkiHexString(s)
Expand All @@ -148,7 +152,6 @@ value class KeypairFingerprintV1String(
) {
companion object {
const val LENGTH = 32
private val pattern = Regex("^[0-9a-f]{22}${SpkiHexString.TRAILING_CONSTANT}\$")

fun fromPublicKeySpki(publicKeySpki: SpkiHexString): KeypairFingerprintV1String {
return KeypairFingerprintV1String(publicKeySpki.s.takeLast(32))
Expand All @@ -160,7 +163,9 @@ value class KeypairFingerprintV1String(
}

init {
require(pattern.matches(s)) { "Invalid fingerprint v1 string: $s" }
require(s.length == LENGTH && s.isValidHex() && s.endsWith(SpkiHexString.TRAILING_CONSTANT)) {
"Invalid fingerprint v1 string: $s"
}
}

fun toV2(): KeypairFingerprintV2String {
Expand All @@ -178,7 +183,7 @@ value class KeypairFingerprintV2String(
val s: String
) {
companion object {
private val pattern = Regex("^[0-9a-f]{22}\$")
private const val LENGTH = 22

fun fromV1(v1: KeypairFingerprintV1String): KeypairFingerprintV2String {
return KeypairFingerprintV2String(v1.s.dropLast(10))
Expand All @@ -190,6 +195,6 @@ value class KeypairFingerprintV2String(
}

init {
require(pattern.matches(s)) { "Invalid fingerprint v2 string: $s" }
require(s.length == LENGTH && s.isValidHex()) { "Invalid fingerprint v2 string: $s" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ object ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ZonedDateTime", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: ZonedDateTime) {
encoder.encodeString(value.toIso8601String())
encoder.encodeString(value.toIso8601AndZoneString())
}

override fun deserialize(decoder: Decoder): ZonedDateTime {
return ZonedDateTime.fromIso8601String(decoder.decodeString())
return ZonedDateTime.fromIso8601AndZoneString(decoder.decodeString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.icure.cardinal.sdk.utils

private val DIGITS_RANGE = '0'.code .. '9'.code
private val LOWERCASE_RANGE = 'a'.code .. 'f'.code
private val UPPERCASE_RANGE = 'A'.code .. 'F'.code

fun String.isValidHex() =
length % 2 == 0 && all { c -> c.code.let { it in DIGITS_RANGE || it in LOWERCASE_RANGE || it in UPPERCASE_RANGE } }
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,45 @@ data class ZonedDateTime(
val zoneOffset: UtcOffset,
val zoneId: TimeZone
) {
fun toIso8601String(): String {
return "$dateTime$zoneOffset[$zoneId]"
fun toIso8601AndZoneString(): String {
return if (zoneOffset.toString() == zoneId.id) {
"$dateTime$zoneOffset"
} else {
"$dateTime$zoneOffset[$zoneId]"
}
}

companion object {
private val isoRegex = "^(?<localTime>[^+]+)(?<offset>[^\\[]+)\\[(?<zoneId>.+)]".toRegex()

fun fromIso8601String(isoDateString: String): ZonedDateTime {
val matches = isoRegex.find(isoDateString)?.groups as? MatchNamedGroupCollection
val localTimeString = requireNotNull(matches?.get("localTime")?.value) { "Cannot extract LocalDateTime" }
val zoneOffsetString = requireNotNull(matches?.get("offset")?.value) { "Cannot extract ZoneOffset" }
val zoneIdString = requireNotNull(matches?.get("zoneId")?.value) { "Cannot extract ZoneId" }
fun fromIso8601AndZoneString(isoAndZoneDateString: String): ZonedDateTime {
val zoneId = if (isoAndZoneDateString.last() == ']') {
val zoneIdStartIndex = isoAndZoneDateString.indexOfLast { it == '[' }.also {
require(it > 0) {
"Invalid isoAndZoneDateString \"$isoAndZoneDateString\""
}
} + 1
require(zoneIdStartIndex < isoAndZoneDateString.length - 1) {
"Invalid isoAndZoneDateString \"$isoAndZoneDateString\""
}
isoAndZoneDateString.substring(zoneIdStartIndex, isoAndZoneDateString.length - 1)
} else null
val stringWithoutZoneId = if (zoneId != null) {
isoAndZoneDateString.substring(0, isoAndZoneDateString.length - zoneId.length - 2)
} else isoAndZoneDateString
val offsetString = if (stringWithoutZoneId.last() == 'Z') {
"Z"
} else {
val offsetStartIndex = stringWithoutZoneId.indexOfLast { it == '+' || it == '-' }.also {
require(it > 0) {
"Invalid isoAndZoneDateString \"$isoAndZoneDateString\""
}
}
stringWithoutZoneId.substring(offsetStartIndex)
}
val localTimeString = stringWithoutZoneId.dropLast(offsetString.length)
return ZonedDateTime(
dateTime = LocalDateTime.parse(localTimeString),
zoneOffset = UtcOffset.parse(zoneOffsetString),
zoneId = TimeZone.of(zoneIdString)
zoneOffset = UtcOffset.parse(offsetString),
zoneId = TimeZone.of(zoneId ?: offsetString)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.icure.cardinal.sdk.utils.time

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe

class ZonedDateTimeTest : StringSpec({
"Full zoned date time test" {
listOf(
Triple("2025-02-24T15:07:01.564571","-05:00", "America/New_York"),
Triple("2025-02-24T15:07:01","Z", "UTC"),
Triple("2025-02-24T15:07:01.564571","+01:00", "UT+01:00"),
).forEach { (time, offset, zone) ->
val format = "$time$offset[$zone]"
val parsed = ZonedDateTime.fromIso8601AndZoneString(format)
parsed.toIso8601AndZoneString() shouldBe format
}
}

"Offset only zone test" {
listOf(
Pair("2025-02-24T15:06:08.886342","+01:00"),
Pair("2025-02-24T15:06:08.886342","-02:30"),
Pair("2025-02-24T12:00:01", "Z")
).forEach { (time, offset) ->
val format = "$time$offset"
val parsed = ZonedDateTime.fromIso8601AndZoneString(format)
parsed.toIso8601AndZoneString() shouldBe format
parsed.zoneId.id shouldBe offset
}
}

"Invalid input test" {
listOf(
"2025-02-24T12:00:12",
"2025-02-24T12:00:12UTC",
"2025-02-24T12:00:12-02:30[Europe/New_York]",
"[America/New_York]",
"-02:30",
).forEach {
shouldThrow<IllegalArgumentException> { ZonedDateTime.fromIso8601AndZoneString(it) }
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ object CheckedConverters {
fun zonedDateTimeToString(
zonedDateTime: ZonedDateTime,
): String {
return zonedDateTime.toIso8601String()
return zonedDateTime.toIso8601AndZoneString()
}

fun zonedDateTimeToString(
Expand All @@ -214,7 +214,7 @@ object CheckedConverters {
description: String
): ZonedDateTime {
return try {
ZonedDateTime.fromIso8601String(string)
ZonedDateTime.fromIso8601AndZoneString(string)
} catch (e: Exception) {
throw IllegalArgumentException("Invalid zoned date time $string @ $description", e)
}
Expand Down

0 comments on commit 47e7f6d

Please sign in to comment.