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 }