libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 373dae59dac29620768c9c4664050129860dc3a7
parent f7629d9be5a3c7853127557ada2543fe251f387a
Author: Antoine A <>
Date:   Mon, 14 Oct 2024 11:45:08 +0200

common: better subject parser

Diffstat:
Mcommon/src/main/kotlin/TxMedatada.kt | 59++++++++++++++++++++++++++++++++++++-----------------------
Mcommon/src/test/kotlin/TxMedataTest.kt | 35++++++++++++++++++++++-------------
2 files changed, 58 insertions(+), 36 deletions(-)

diff --git a/common/src/main/kotlin/TxMedatada.kt b/common/src/main/kotlin/TxMedatada.kt @@ -19,31 +19,35 @@ package tech.libeufin.common -private val BASE32_32B_UPPER_PATTERN = Regex("(KYC:)?([0-9A-Z]{52})") +private val PART_PATTERN = Regex("[:a-z0-9A-Z]*") private val BASE32_32B_PATTERN = Regex("(KYC:)?([a-z0-9A-Z]{52})") -private val CLEAN_PATTERN = Regex(" ?[\\n\\-\\+] ?") data class TalerIncomingMetadata(val type: TalerIncomingType, val key: EddsaPublicKey) /** * Extract the reserve public key from an incoming Taler transaction subject * - * We first try to match an uppercase key then a lowercase key. If none are - * found we clean the subject and retry. + * We first try to find a whole key. If none are found we try to reconstruct + * one from parts. **/ fun parseIncomingTxMetadata(subject: String): TalerIncomingMetadata { - /** - * Extract the reserve public key from [subject] using [pattern] - * - * Return null if found none and throw if find too many - **/ - fun run(subject: String, pattern: Regex): TalerIncomingMetadata? { - val matches = pattern.findAll(subject).iterator() - if (!matches.hasNext()) return null - val match = matches.next() - if (matches.hasNext()) { + /** Check one or no reserve public key is found **/ + fun check(matches: Sequence<MatchResult>): TalerIncomingMetadata? { + val iterator = matches.iterator() + + // Check none + if (!iterator.hasNext()) { + return null + } + + val match = iterator.next() + + // Check many + if (iterator.hasNext()) { throw Exception("Found multiple reserve public key") } + + // Check kind val (prefix, key) = match.destructured val type = if (prefix == "KYC:") TalerIncomingType.kyc else TalerIncomingType.reserve return TalerIncomingMetadata(type, EddsaPublicKey(key)) @@ -61,17 +65,26 @@ fun parseIncomingTxMetadata(subject: String): TalerIncomingMetadata { // Since any sequence of 52 upper and lowercase characters can be a // valid encoded key, deleting spaces and separators can create false - // positives, so we always start by searching for a key coded in - // uppercase and then mixed once. + // positives, so we always start by searching for a valid whole key + // then we try to reconstruct a key from valid parts. + + // Whole key match + val key = check(BASE32_32B_PATTERN.findAll(subject)) + if (key != null) return key - val matchWithoutCleaning = run(subject, BASE32_32B_UPPER_PATTERN) - ?: run(subject, BASE32_32B_PATTERN) - if (matchWithoutCleaning != null) return matchWithoutCleaning + // Key reconstruction from parts + val parts = PART_PATTERN.findAll(subject).map { it.value }.toList() + for (windowSize in 2..parts.size) { + val matches = parts.windowed(windowSize).asSequence().map { window -> + val joined = window.joinToString("") + BASE32_32B_PATTERN.matchEntire(joined) + }.filterNotNull() + val key = check(matches) + if (key != null) return key + } - val cleaned = subject.replace(CLEAN_PATTERN, "") - return run(cleaned, BASE32_32B_UPPER_PATTERN) - ?: run(cleaned, BASE32_32B_PATTERN) - ?: throw Exception("Missing reserve public key") + // No key where found + throw Exception("Missing reserve public key") } /** Extract the reserve public key from an incoming Taler transaction subject */ diff --git a/common/src/test/kotlin/TxMedataTest.kt b/common/src/test/kotlin/TxMedataTest.kt @@ -18,9 +18,7 @@ */ import tech.libeufin.common.* -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFails +import kotlin.test.* class TxMetadataTest { fun assertFailsMsg(msg: String, lambda: () -> Unit) { @@ -56,6 +54,7 @@ class TxMetadataTest { for ((L, R) in sequenceOf(upperL to upperR, mixedL to mixedR)) { for (case in sequenceOf( "left $L$R right", + "left $L $R right", "left $L-$R right", "left $L+$R right", "left $L\n$R right", @@ -67,20 +66,14 @@ class TxMetadataTest { )) { assertEquals(key, parseIncomingTxMetadata(case)) } - } - - // Check prefer upper - for (case in sequenceOf( - "$mixed $upper", - "$mixedL-$mixedR $upperL-$upperR" - )) { - assertEquals(key, parseIncomingTxMetadata(case)) } // Check failure when multiple keys match for (case in sequenceOf( - "$upper $upper", // Both upper - "$mixed $mixed" // Both mixed + "$upper $upper", + "$mixed $mixed", + "$mixed $upper", + "$mixedL-$mixedR $upperL-$upperR" )) { assertFailsMsg("Found multiple reserve public key") { parseIncomingTxMetadata(case) @@ -97,4 +90,20 @@ class TxMetadataTest { } } } + + /** Test parsing logic using real use case */ + @Test + fun real() { + // Good cases + for ((subject, key) in sequenceOf( + "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60" to "TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60", + "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG" to "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG", + "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0", + )) { + assertEquals( + TalerIncomingMetadata(TalerIncomingType.reserve, EddsaPublicKey(key)), + parseIncomingTxMetadata(subject) + ) + } + } } \ No newline at end of file