libeufin

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

helpers.kt (6281B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.common
     21 
     22 import io.ktor.server.application.*
     23 import org.slf4j.Logger
     24 import java.io.ByteArrayOutputStream
     25 import java.io.FilterInputStream
     26 import java.io.InputStream
     27 import java.math.BigInteger
     28 import java.security.SecureRandom
     29 import java.time.Instant
     30 import java.time.LocalDate
     31 import java.time.LocalDateTime
     32 import java.time.ZoneOffset
     33 import java.time.format.DateTimeFormatter
     34 import java.util.*
     35 import java.util.zip.DeflaterInputStream
     36 import java.util.zip.InflaterInputStream
     37 import java.util.zip.ZipInputStream
     38 import kotlin.random.Random
     39 
     40 /* ----- String ----- */
     41 
     42 /** Decode a base64 encoded string */
     43 fun String.decodeBase64(): ByteArray = Base64.getDecoder().decode(this)
     44 /** Encode a string as base64 */
     45 fun String.encodeBase64(): String = toByteArray().encodeBase64()
     46 /** Decode a hexadecimal uppercase encoded string */
     47 fun String.decodeUpHex(): ByteArray = HexFormat.of().withUpperCase().parseHex(this)
     48 
     49 fun String.splitOnce(pat: String): Pair<String, String>? {
     50     val split = splitToSequence(pat, limit=2).iterator()
     51     val first = split.next()
     52     if (!split.hasNext()) return null
     53     return Pair(first, split.next())
     54 }
     55 
     56 /** Format a string with a space every two characters */
     57 fun String.fmtChunkByTwo() = buildString {
     58     this@fmtChunkByTwo.forEachIndexed { pos, c ->
     59         if (pos != 0 && pos % 2 == 0) append(' ')
     60         append(c)
     61     }
     62 }
     63 
     64 /* ----- Date & Time ----- */
     65 
     66 /** Converting YYYY-MM-DD to Instant */
     67 fun dateToInstant(date: String): Instant = 
     68     LocalDate.parse(date, DateTimeFormatter.ISO_DATE).atStartOfDay().toInstant(ZoneOffset.UTC)
     69 
     70 /** Converting YYYY-MM-DDTHH:MM:SS to Instant */
     71 fun dateTimeToInstant(date: String): Instant = 
     72     LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE_TIME).toInstant(ZoneOffset.UTC)
     73 
     74 private val DATE_TIME_PATH = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HHmmss")
     75 
     76 /** Converting Instant to YYYY-MM-DDTHHMMSS */
     77 fun Instant.toDateTimeFilePath(): String =
     78     this.atOffset(ZoneOffset.UTC).format(DATE_TIME_PATH)
     79 
     80 /* ----- BigInteger -----*/
     81 
     82 fun BigInteger.encodeHex(): String = this.toByteArray().encodeHex()
     83 fun BigInteger.encodeBase64(): String = this.toByteArray().encodeBase64()
     84 
     85 /* ----- Random ----- */
     86 
     87 /** Thread local cryptographically strong random number generator */
     88 val SECURE_RNG = ThreadLocal.withInitial { SecureRandom() }
     89 
     90 /* ----- ByteArray ----- */
     91 
     92 fun ByteArray.rand(rng: Random = Random): ByteArray {
     93     rng.nextBytes(this)
     94     return this
     95 }
     96 fun ByteArray.secureRand(): ByteArray {
     97     SECURE_RNG.get().nextBytes(this)
     98     return this
     99 }
    100 fun ByteArray.encodeHex(): String = HexFormat.of().formatHex(this)
    101 fun ByteArray.encodeUpHex(): String = HexFormat.of().withUpperCase().formatHex(this)
    102 fun ByteArray.encodeBase64(): String = Base64.getEncoder().encodeToString(this)
    103 fun ByteArray.asUtf8(): String = this.toString(Charsets.UTF_8)
    104 fun ByteArrayOutputStream.asUtf8(): String = this.toString(Charsets.UTF_8)
    105 
    106 /* ----- InputStream ----- */
    107 
    108 /** Unzip an input stream and run [lambda] over each entry */
    109 inline fun InputStream.unzipEach(lambda: (String, InputStream) -> Unit) {
    110     ZipInputStream(this).use { zip ->
    111         while (true) {
    112             val entry = zip.getNextEntry() ?: break
    113             val entryStream = object: FilterInputStream(zip) {
    114                 override fun close() {
    115                     zip.closeEntry()
    116                 }
    117             }
    118             lambda(entry.name, entryStream)
    119         }
    120     }
    121 }
    122 
    123 /** Decode a base64 encoded input stream */
    124 fun InputStream.decodeBase64(): InputStream 
    125     = Base64.getDecoder().wrap(this)
    126 
    127 /** Encode an input stream as base64 */
    128 fun InputStream.encodeBase64(): String {
    129     val w = ByteArrayOutputStream()
    130     val encoded = Base64.getEncoder().wrap(w)
    131     transferTo(encoded)
    132     encoded.close()
    133     return w.asUtf8()
    134 }
    135 
    136 /** Deflate an input stream */
    137 fun InputStream.deflate(): DeflaterInputStream 
    138     = DeflaterInputStream(this)
    139 
    140 /** Inflate an input stream */
    141 fun InputStream.inflate(): InflaterInputStream 
    142     = InflaterInputStream(this)
    143 
    144 /** Read an input stream as UTF8 text */
    145 fun InputStream.readText(): String 
    146     = this.reader().readText()
    147 
    148 /* ----- Throwable ----- */
    149 
    150 fun Throwable.fmt(): String = buildString {
    151     append(message ?: this@fmt::class.simpleName)
    152     var cause = cause
    153     while (cause != null) {
    154         append(": ")
    155         append(cause.message ?: cause::class.simpleName)
    156         cause = cause.cause
    157     }
    158 }
    159 
    160 fun Throwable.fmtLog(logger: Logger) {
    161     logger.error(this.fmt())
    162     logger.trace("", this)
    163 }
    164 
    165 /* ----- Logger ----- */
    166 
    167 inline fun Logger.debug(lambda: () -> String) {
    168     if (isDebugEnabled) debug(lambda())
    169 }
    170 
    171 inline fun Logger.trace(lambda: () -> String) {
    172     if (isTraceEnabled) trace(lambda())
    173 }
    174 
    175 /* ----- KTOR ----- */
    176 
    177 fun ApplicationCall.uuidPath(name: String): UUID {
    178     val value = parameters[name]!!
    179     try {
    180         return UUID.fromString(value)
    181     } catch (e: Exception) {
    182         throw badRequest("UUID uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ?
    183     }
    184 }
    185 
    186 fun ApplicationCall.longPath(name: String): Long {
    187     val value = parameters[name]!!
    188     try {
    189         return value.toLong()
    190     } catch (e: Exception) {
    191         throw badRequest("Long uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ?
    192     }
    193 }
    194 
    195 /* ----- Payto ----- */
    196 
    197 fun ibanPayto(iban: String, name: String? = null): IbanPayto {
    198     return Payto.parse(IbanPayto.build(iban, null, name)).expectIban()
    199 }