From 023ea96d26a65d29d408ef78c9405411bb842afd Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 16 May 2022 19:58:31 +0200 Subject: -remove multiplatform dependency, library upgrade WIP --- .../src/main/java/net/taler/common/Amount.kt | 199 +++++++++++++++++++++ .../src/main/java/net/taler/common/AndroidUtils.kt | 1 - .../main/java/net/taler/common/ContractTerms.kt | 2 - .../src/main/java/net/taler/common/TalerUri.kt | 62 +++++++ .../src/main/java/net/taler/common/Time.kt | 129 +++++++++++++ .../src/main/java/net/taler/common/Version.kt | 70 ++++++++ 6 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/Amount.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/Time.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/Version.kt (limited to 'taler-kotlin-android/src/main/java/net/taler/common') diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt new file mode 100644 index 0000000..a36c0ab --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt @@ -0,0 +1,199 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.common + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.math.floor +import kotlin.math.pow +import kotlin.math.roundToInt + +public class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) +public class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) + +@Serializable(with = KotlinXAmountSerializer::class) +public data class Amount( + /** + * name of the currency using either a three-character ISO 4217 currency code, + * or a regional currency identifier starting with a "*" followed by at most 10 characters. + * ISO 4217 exponents in the name are not supported, + * although the "fraction" is corresponds to an ISO 4217 exponent of 6. + */ + val currency: String, + + /** + * The integer part may be at most 2^52. + * Note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent. + */ + val value: Long, + + /** + * Unsigned 32 bit fractional value to be added to value representing + * an additional currency fraction, in units of one hundred millionth (1e-8) + * of the base currency value. For example, a fraction + * of 50_000_000 would correspond to 50 cents. + */ + val fraction: Int +) : Comparable { + + public companion object { + + private const val FRACTIONAL_BASE: Int = 100000000 // 1e8 + + private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""") + public val MAX_VALUE: Long = 2.0.pow(52).toLong() + private const val MAX_FRACTION_LENGTH = 8 + public const val MAX_FRACTION: Int = 99_999_999 + + public fun zero(currency: String): Amount { + return Amount(checkCurrency(currency), 0, 0) + } + + public fun fromJSONString(str: String): Amount { + val split = str.split(":") + if (split.size != 2) throw AmountParserException("Invalid Amount Format") + return fromString(split[0], split[1]) + } + + public fun fromString(currency: String, str: String): Amount { + // value + val valueSplit = str.split(".") + val value = checkValue(valueSplit[0].toLongOrNull()) + // fraction + val fraction: Int = if (valueSplit.size > 1) { + val fractionStr = valueSplit[1] + if (fractionStr.length > MAX_FRACTION_LENGTH) + throw AmountParserException("Fraction $fractionStr too long") + val fraction = "0.$fractionStr".toDoubleOrNull() + ?.times(FRACTIONAL_BASE) + ?.roundToInt() + checkFraction(fraction) + } else 0 + return Amount(checkCurrency(currency), value, fraction) + } + + public fun min(currency: String): Amount = Amount(currency, 0, 1) + public fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION) + + + internal fun checkCurrency(currency: String): String { + if (!REGEX_CURRENCY.matches(currency)) + throw AmountParserException("Invalid currency: $currency") + return currency + } + + internal fun checkValue(value: Long?): Long { + if (value == null || value > MAX_VALUE) + throw AmountParserException("Value $value greater than $MAX_VALUE") + return value + } + + internal fun checkFraction(fraction: Int?): Int { + if (fraction == null || fraction > MAX_FRACTION) + throw AmountParserException("Fraction $fraction greater than $MAX_FRACTION") + return fraction + } + + } + + public val amountStr: String + get() = if (fraction == 0) "$value" else { + var f = fraction + var fractionStr = "" + while (f > 0) { + fractionStr += f / (FRACTIONAL_BASE / 10) + f = (f * 10) % FRACTIONAL_BASE + } + "$value.$fractionStr" + } + + public operator fun plus(other: Amount): Amount { + check(currency == other.currency) { "Can only subtract from same currency" } + val resultValue = value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong() + if (resultValue > MAX_VALUE) + throw AmountOverflowException() + val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE + return Amount(currency, resultValue, resultFraction) + } + + public operator fun times(factor: Int): Amount { + // TODO consider replacing with a faster implementation + if (factor == 0) return zero(currency) + var result = this + for (i in 1 until factor) result += this + return result + } + + public operator fun minus(other: Amount): Amount { + check(currency == other.currency) { "Can only subtract from same currency" } + var resultValue = value + var resultFraction = fraction + if (resultFraction < other.fraction) { + if (resultValue < 1L) + throw AmountOverflowException() + resultValue-- + resultFraction += FRACTIONAL_BASE + } + check(resultFraction >= other.fraction) + resultFraction -= other.fraction + if (resultValue < other.value) + throw AmountOverflowException() + resultValue -= other.value + return Amount(currency, resultValue, resultFraction) + } + + public fun isZero(): Boolean { + return value == 0L && fraction == 0 + } + + public fun toJSONString(): String { + return "$currency:$amountStr" + } + + override fun toString(): String { + return "$amountStr $currency" + } + + override fun compareTo(other: Amount): Int { + check(currency == other.currency) { "Can only compare amounts with the same currency" } + when { + value == other.value -> { + if (fraction < other.fraction) return -1 + if (fraction > other.fraction) return 1 + return 0 + } + value < other.value -> return -1 + else -> return 1 + } + } + +} + +@Suppress("EXPERIMENTAL_API_USAGE") +@Serializer(forClass = Amount::class) +internal object KotlinXAmountSerializer : KSerializer { + override fun serialize(encoder: Encoder, value: Amount) { + encoder.encodeString(value.toJSONString()) + } + + override fun deserialize(decoder: Decoder): Amount { + return Amount.fromJSONString(decoder.decodeString()) + } +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt index 4ac2e73..d86e744 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt @@ -49,7 +49,6 @@ import androidx.navigation.fragment.findNavController import com.github.pedrovgs.lynx.LynxActivity import com.github.pedrovgs.lynx.LynxConfig import net.taler.lib.android.ErrorBottomSheet -import net.taler.lib.common.Version fun View.fadeIn(endAction: () -> Unit = {}) { if (visibility == VISIBLE && alpha == 1f) return diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt index fb30692..910cc36 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt @@ -20,8 +20,6 @@ import android.os.Build import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.taler.common.TalerUtils.getLocalizedString -import net.taler.lib.common.Amount -import net.taler.lib.common.Timestamp @Serializable data class ContractTerms( diff --git a/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt b/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt new file mode 100644 index 0000000..a1b7225 --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt @@ -0,0 +1,62 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.common + +import java.util.Locale + +public object TalerUri { + + private const val SCHEME = "taler://" + private const val SCHEME_INSECURE = "taler+http://" + private const val AUTHORITY_PAY = "pay" + private const val AUTHORITY_WITHDRAW = "withdraw" + private const val AUTHORITY_REFUND = "refund" + private const val AUTHORITY_TIP = "tip" + + public data class WithdrawUriResult( + val bankIntegrationApiBaseUrl: String, + val withdrawalOperationId: String + ) + + /** + * Parses a withdraw URI and returns a bank status URL or null if the URI was invalid. + */ + public fun parseWithdrawUri(uri: String): WithdrawUriResult? { + val (resultScheme, prefix) = when { + uri.startsWith(SCHEME, ignoreCase = true) -> { + Pair("https://", "${SCHEME}${AUTHORITY_WITHDRAW}/") + } + uri.startsWith(SCHEME_INSECURE, ignoreCase = true) -> { + Pair("http://", "${SCHEME_INSECURE}${AUTHORITY_WITHDRAW}/") + } + else -> return null + } + if (!uri.startsWith(prefix)) return null + val parts = uri.let { + (if (it.endsWith("/")) it.dropLast(1) else it).substring(prefix.length).split('/') + } + if (parts.size < 2) return null + val host = parts[0].lowercase(Locale.ROOT) + val pathSegments = parts.slice(1 until parts.size - 1).joinToString("/") + val withdrawId = parts.last() + if (withdrawId.isBlank()) return null + val url = "${resultScheme}${host}/${pathSegments}" + + return WithdrawUriResult(url, withdrawId) + } + +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Time.kt b/taler-kotlin-android/src/main/java/net/taler/common/Time.kt new file mode 100644 index 0000000..61bbce8 --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/Time.kt @@ -0,0 +1,129 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.common + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.JsonTransformingSerializer +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import kotlin.math.max + +@Serializable +data class Timestamp( + @SerialName("t_ms") + @Serializable(NeverSerializer::class) + val old_ms: Long? = null, + @SerialName("t_s") + @Serializable(NeverSerializer::class) + private val s: Long? = null, +) : Comparable { + + constructor(ms: Long) : this(ms, null) + + companion object { + private const val NEVER: Long = -1 + fun now(): Timestamp = Timestamp(System.currentTimeMillis()) + fun never(): Timestamp = Timestamp(NEVER) + } + + val ms: Long = if (s != null) { + s * 1000L + } else if (old_ms !== null) { + old_ms + } else { + throw Exception("timestamp didn't have t_s or t_ms") + } + + + /** + * Returns a copy of this [Timestamp] rounded to seconds. + */ + fun truncateSeconds(): Timestamp { + if (ms == NEVER) return Timestamp(ms) + return Timestamp((ms / 1000L) * 1000L) + } + + operator fun minus(other: Timestamp): Duration = when { + ms == NEVER -> Duration(Duration.FOREVER) + other.ms == NEVER -> throw Error("Invalid argument for timestamp comparision") + ms < other.ms -> Duration(0) + else -> Duration(ms - other.ms) + } + + operator fun minus(other: Duration): Timestamp = when { + ms == NEVER -> this + other.ms == Duration.FOREVER -> Timestamp(0) + else -> Timestamp(max(0, ms - other.ms)) + } + + override fun compareTo(other: Timestamp): Int { + return if (ms == NEVER) { + if (other.ms == NEVER) 0 + else 1 + } else { + if (other.ms == NEVER) -1 + else ms.compareTo(other.ms) + } + } + +} + +@Serializable +data class Duration( + @SerialName("d_ms") + @Serializable(ForeverSerializer::class) val old_ms: Long? = null, + @SerialName("d_s") + @Serializable(ForeverSerializer::class) + private val s: Long? = null, +) { + val ms: Long = if (s != null) { + s * 1000L + } else if (old_ms !== null) { + old_ms + } else { + throw Exception("duration didn't have d_s or d_ms") + } + + constructor(ms: Long) : this(ms, null) + + companion object { + internal const val FOREVER: Long = -1 + fun forever(): Duration = Duration(FOREVER) + } +} + +internal abstract class MinusOneSerializer(private val keyword: String) : + JsonTransformingSerializer(Long.serializer()) { + + override fun transformDeserialize(element: JsonElement): JsonElement { + return if (element.jsonPrimitive.contentOrNull == keyword) return JsonPrimitive(-1) + else super.transformDeserialize(element) + } + + override fun transformSerialize(element: JsonElement): JsonElement { + return if (element.jsonPrimitive.longOrNull == -1L) return JsonPrimitive(keyword) + else element + } +} + +internal object NeverSerializer : MinusOneSerializer("never") +internal object ForeverSerializer : MinusOneSerializer("forever") diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Version.kt b/taler-kotlin-android/src/main/java/net/taler/common/Version.kt new file mode 100644 index 0000000..f46913e --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/Version.kt @@ -0,0 +1,70 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.common + +import kotlin.math.sign + +/** + * Semantic versioning, but libtool-style. + * See https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html + */ +public data class Version( + val current: Int, + val revision: Int, + val age: Int +) { + public companion object { + public fun parse(v: String): Version? { + val elements = v.split(":") + if (elements.size != 3) return null + val (currentStr, revisionStr, ageStr) = elements + val current = currentStr.toIntOrNull() + val revision = revisionStr.toIntOrNull() + val age = ageStr.toIntOrNull() + if (current == null || revision == null || age == null) return null + return Version(current, revision, age) + } + } + + /** + * Compare two libtool-style versions. + * + * Returns a [VersionMatchResult] or null if the given version was null. + */ + public fun compare(other: Version?): VersionMatchResult? { + if (other == null) return null + val compatible = current - age <= other.current && + current >= other.current - other.age + val currentCmp = sign((current - other.current).toDouble()).toInt() + return VersionMatchResult(compatible, currentCmp) + } + + /** + * Result of comparing two libtool versions. + */ + public data class VersionMatchResult( + /** + * Is the first version compatible with the second? + */ + val compatible: Boolean, + /** + * Is the first version older (-1), newer (+1) or identical (0)? + */ + val currentCmp: Int + ) + +} -- cgit v1.2.3