feat: add linuxX64 KMP target and restructure native source sets
Restructure quartz module's Kotlin/Native source set hierarchy for
proper multiplatform coverage. Adds linuxX64 target with complete
actual implementations for all expect declarations.
Source set hierarchy:
commonMain
├── jvmAndroid → jvmMain, androidMain
└── nativeMain (shared pure Kotlin)
├── appleMain (Apple APIs) → iosMain
└── linuxMain (POSIX/OpenSSL) → linuxX64Main
nativeMain: Address, OptimizedJsonMapper, EventHasherSerializer,
ChessEngine, Secp256k1Instance, BitSet, StringExt, io/ utilities
appleMain: Log (NSLog), SecureRandom (SecRandomCopyBytes),
UriParser (NSURLComponents), BigDecimal (NSDecimalNumber),
GZip (zlib), Sha256 (CC_SHA256), crypto (Apple provider),
LargeCache (CacheMap), UrlEncoder, UnicodeNormalizer
linuxMain: Log (println), SecureRandom (/dev/urandom),
UriParser (pure Kotlin), BigDecimal (pure Kotlin),
GZip (zlib cinterop), Sha256 (OpenSSL), crypto (OpenSSL),
LargeCache (AtomicReference<LinkedHashMap>), UrlEncoder
macOS targets deferred pending negentropy-kmp macOS artifacts.
macosMain/Platform.kt retained as scaffold for future activation.
https://claude.ai/code/session_01M9CAuPUV3TfPB2xShnsBhw
This commit is contained in:
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip96FileStorage.info
|
||||
|
||||
import platform.Foundation.NSURL
|
||||
import platform.Foundation.NSURLComponents
|
||||
|
||||
actual fun makeAbsoluteIfRelativeUrl(
|
||||
baseUrl: String,
|
||||
potentiallyRelativeUrl: String,
|
||||
): String =
|
||||
try {
|
||||
val refUrl = NSURLComponents(potentiallyRelativeUrl)
|
||||
if (refUrl.scheme != null) {
|
||||
potentiallyRelativeUrl
|
||||
} else {
|
||||
val apiUrlComponents =
|
||||
NSURLComponents(
|
||||
uRL =
|
||||
NSURL(
|
||||
string = potentiallyRelativeUrl,
|
||||
relativeToURL = NSURL(baseUrl, encodingInvalidCharacters = false),
|
||||
),
|
||||
resolvingAgainstBaseURL = true,
|
||||
)
|
||||
apiUrlComponents.string ?: throw Exception("URL resolution failed.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
potentiallyRelativeUrl
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import platform.Foundation.NSDecimalNumber
|
||||
|
||||
// class from https://github.com/apollographql/apollo-kotlin-adapters
|
||||
actual class BigDecimal internal constructor(
|
||||
private val raw: NSDecimalNumber,
|
||||
) : Number() {
|
||||
actual constructor(strVal: String) : this(NSDecimalNumber(strVal))
|
||||
|
||||
actual constructor(doubleVal: Double) : this(NSDecimalNumber(doubleVal))
|
||||
|
||||
actual constructor(intVal: Int) : this(NSDecimalNumber(int = intVal))
|
||||
|
||||
actual constructor(longVal: Long) : this(NSDecimalNumber(longLong = longVal))
|
||||
|
||||
actual fun add(augend: BigDecimal): BigDecimal = BigDecimal(raw.decimalNumberByAdding(augend.raw))
|
||||
|
||||
actual fun subtract(subtrahend: BigDecimal): BigDecimal = BigDecimal(raw.decimalNumberBySubtracting(subtrahend.raw))
|
||||
|
||||
actual fun multiply(multiplicand: BigDecimal): BigDecimal = BigDecimal(raw.decimalNumberByMultiplyingBy(multiplicand.raw))
|
||||
|
||||
actual fun divide(divisor: BigDecimal): BigDecimal = BigDecimal(raw.decimalNumberByDividingBy(divisor.raw))
|
||||
|
||||
actual fun negate(): BigDecimal = BigDecimal(NSDecimalNumber(int = 0).decimalNumberBySubtracting(raw))
|
||||
|
||||
actual fun signum(): Int {
|
||||
val result = raw.compare(NSDecimalNumber(int = 0)).toInt()
|
||||
return when {
|
||||
result < 0 -> -1
|
||||
result > 0 -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun toInt(): Int = raw.intValue
|
||||
|
||||
override fun toLong(): Long = raw.longLongValue
|
||||
|
||||
override fun toShort(): Short = raw.shortValue
|
||||
|
||||
override fun toByte(): Byte = raw.charValue
|
||||
|
||||
override fun toDouble(): Double = raw.doubleValue
|
||||
|
||||
override fun toFloat(): Float = raw.floatValue
|
||||
|
||||
override fun equals(other: Any?): Boolean = (this === other) || raw == (other as? BigDecimal)?.raw
|
||||
|
||||
override fun hashCode(): Int = raw.hashCode()
|
||||
|
||||
override fun toString(): String = raw.stringValue
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.ptr
|
||||
import kotlinx.cinterop.reinterpret
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.zlib.Z_DEFAULT_COMPRESSION
|
||||
import platform.zlib.Z_DEFAULT_STRATEGY
|
||||
import platform.zlib.Z_DEFLATED
|
||||
import platform.zlib.Z_FINISH
|
||||
import platform.zlib.Z_NO_FLUSH
|
||||
import platform.zlib.Z_OK
|
||||
import platform.zlib.Z_STREAM_END
|
||||
import platform.zlib.deflate
|
||||
import platform.zlib.deflateBound
|
||||
import platform.zlib.deflateEnd
|
||||
import platform.zlib.deflateInit2
|
||||
import platform.zlib.inflate
|
||||
import platform.zlib.inflateEnd
|
||||
import platform.zlib.inflateInit2
|
||||
import platform.zlib.z_stream
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual object GZip {
|
||||
/**
|
||||
* Compresses [content] into a gzip byte array using zlib's deflateInit2 with
|
||||
* windowBits=31 (MAX_WBITS + 16), which requests the gzip wrapper format.
|
||||
* deflateBound gives an upper-bound on output size so a single deflate call
|
||||
* with Z_FINISH is always sufficient.
|
||||
*/
|
||||
actual fun compress(content: String): ByteArray {
|
||||
val input = content.encodeToByteArray()
|
||||
|
||||
return memScoped {
|
||||
val stream = alloc<z_stream>()
|
||||
|
||||
// windowBits = 15 + 16 = 31 → gzip format
|
||||
deflateInit2(
|
||||
stream.ptr,
|
||||
Z_DEFAULT_COMPRESSION,
|
||||
Z_DEFLATED,
|
||||
31,
|
||||
8,
|
||||
Z_DEFAULT_STRATEGY,
|
||||
).let { check(it == Z_OK) { "deflateInit2 failed: $it" } }
|
||||
|
||||
val maxSize = deflateBound(stream.ptr, input.size.toULong()).toInt()
|
||||
val output = ByteArray(maxSize)
|
||||
|
||||
val written =
|
||||
input.usePinned { pinIn ->
|
||||
output.usePinned { pinOut ->
|
||||
if (input.isNotEmpty()) {
|
||||
stream.next_in = pinIn.addressOf(0).reinterpret()
|
||||
}
|
||||
stream.avail_in = input.size.toUInt()
|
||||
|
||||
if (output.isNotEmpty()) {
|
||||
stream.next_out = pinOut.addressOf(0).reinterpret()
|
||||
}
|
||||
stream.avail_out = maxSize.toUInt()
|
||||
|
||||
deflate(stream.ptr, Z_FINISH)
|
||||
.let { check(it == Z_STREAM_END) { "deflate failed: $it" } }
|
||||
|
||||
maxSize - stream.avail_out.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
deflateEnd(stream.ptr)
|
||||
output.copyOf(written)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompresses a gzip [content] byte array back to a UTF-8 string using
|
||||
* zlib's inflateInit2 with windowBits=47 (MAX_WBITS + 32), which enables
|
||||
* automatic detection of both gzip and zlib formats.
|
||||
* Output is collected in fixed-size chunks to handle arbitrary output size.
|
||||
*/
|
||||
actual fun decompress(content: ByteArray): String {
|
||||
if (content.isEmpty()) return ""
|
||||
|
||||
val chunks = ArrayList<ByteArray>()
|
||||
val chunkSize = maxOf(content.size * 4, 4096)
|
||||
|
||||
memScoped {
|
||||
val stream = alloc<z_stream>()
|
||||
|
||||
// windowBits = 15 + 32 = 47 → auto-detect gzip or zlib
|
||||
inflateInit2(stream.ptr, 47)
|
||||
.let { check(it == Z_OK) { "inflateInit2 failed: $it" } }
|
||||
|
||||
content.usePinned { pinIn ->
|
||||
if (content.isNotEmpty()) {
|
||||
stream.next_in = pinIn.addressOf(0).reinterpret()
|
||||
}
|
||||
stream.avail_in = content.size.toUInt()
|
||||
|
||||
var status: Int = Z_OK
|
||||
do {
|
||||
val chunk = ByteArray(chunkSize)
|
||||
chunk.usePinned { pinOut ->
|
||||
stream.next_out = pinOut.addressOf(0).reinterpret()
|
||||
stream.avail_out = chunkSize.toUInt()
|
||||
status = inflate(stream.ptr, Z_NO_FLUSH)
|
||||
val produced = chunkSize - stream.avail_out.toInt()
|
||||
if (produced > 0) chunks.add(chunk.copyOf(produced))
|
||||
}
|
||||
} while (status == Z_OK)
|
||||
|
||||
check(status == Z_STREAM_END) { "inflate failed: $status" }
|
||||
}
|
||||
|
||||
inflateEnd(stream.ptr)
|
||||
}
|
||||
|
||||
val totalSize = chunks.sumOf { it.size }
|
||||
val result = ByteArray(totalSize)
|
||||
var pos = 0
|
||||
chunks.forEach { chunk ->
|
||||
chunk.copyInto(result, pos)
|
||||
pos += chunk.size
|
||||
}
|
||||
return result.decodeToString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import platform.Foundation.NSLog
|
||||
|
||||
actual object Log {
|
||||
actual fun w(
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable?,
|
||||
) {
|
||||
if (throwable != null) {
|
||||
NSLog("WARN: [$tag] $message. Throwable: $throwable CAUSE ${throwable.cause}")
|
||||
} else {
|
||||
NSLog("WARN: [$tag] $message")
|
||||
}
|
||||
}
|
||||
|
||||
actual fun e(
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable?,
|
||||
) {
|
||||
if (throwable != null) {
|
||||
NSLog("ERROR: [$tag] $message. Throwable: $throwable CAUSE ${throwable.cause}")
|
||||
} else {
|
||||
NSLog("ERROR: [$tag] $message")
|
||||
}
|
||||
}
|
||||
|
||||
actual fun d(
|
||||
tag: String,
|
||||
message: String,
|
||||
) {
|
||||
NSLog("DEBUG: [$tag] $message")
|
||||
}
|
||||
|
||||
actual fun i(
|
||||
tag: String,
|
||||
message: String,
|
||||
) {
|
||||
NSLog("INFO: [$tag] $message")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.refTo
|
||||
import platform.Security.SecRandomCopyBytes
|
||||
import platform.Security.kSecRandomDefault
|
||||
|
||||
actual class SecureRandom {
|
||||
actual fun nextInt(): Int {
|
||||
val bytes = ByteArray(4)
|
||||
nextBytes(bytes)
|
||||
return ((bytes[0].toInt() and 0xFF) shl 24) or
|
||||
((bytes[1].toInt() and 0xFF) shl 16) or
|
||||
((bytes[2].toInt() and 0xFF) shl 8) or
|
||||
(bytes[3].toInt() and 0xFF)
|
||||
}
|
||||
|
||||
actual fun nextInt(bound: Int): Int {
|
||||
require(bound > 0) { throw IllegalArgumentException("Bad Bound $bound") }
|
||||
|
||||
var intValue = nextPositiveInt()
|
||||
|
||||
val m = bound - 1
|
||||
if ((bound and m) == 0) {
|
||||
// i.e., bound is a power of 2
|
||||
intValue = ((bound * intValue.toLong()) shr 31).toInt()
|
||||
} else { // reject over-represented candidates
|
||||
var u: Int = intValue
|
||||
intValue = u % bound
|
||||
while (u - intValue + m < 0) {
|
||||
u = nextPositiveInt()
|
||||
intValue = u % bound
|
||||
}
|
||||
}
|
||||
|
||||
return intValue
|
||||
}
|
||||
|
||||
actual fun nextLong(): Long {
|
||||
val bytes = ByteArray(8)
|
||||
nextBytes(bytes)
|
||||
return ((bytes[0].toLong() and 0xFFL) shl 56) or
|
||||
((bytes[1].toLong() and 0xFFL) shl 48) or
|
||||
((bytes[2].toLong() and 0xFFL) shl 40) or
|
||||
((bytes[3].toLong() and 0xFFL) shl 32) or
|
||||
((bytes[4].toLong() and 0xFFL) shl 24) or
|
||||
((bytes[5].toLong() and 0xFFL) shl 16) or
|
||||
((bytes[6].toLong() and 0xFFL) shl 8) or
|
||||
(bytes[7].toLong() and 0xFFL)
|
||||
}
|
||||
|
||||
actual fun nextLong(bound: Long): Long {
|
||||
require(bound > 0) { throw IllegalArgumentException("Bad Bound $bound") }
|
||||
|
||||
if (bound < Int.MAX_VALUE) {
|
||||
return nextInt(bound.toInt()).toLong()
|
||||
}
|
||||
|
||||
return nextPositiveLong() % bound
|
||||
}
|
||||
|
||||
fun nextPositiveInt(): Int {
|
||||
val value = nextInt()
|
||||
return if (value > 0) {
|
||||
value
|
||||
} else {
|
||||
-value
|
||||
}
|
||||
}
|
||||
|
||||
fun nextPositiveLong(): Long {
|
||||
val value = nextLong()
|
||||
return if (value > 0) {
|
||||
value
|
||||
} else {
|
||||
-value
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun nextBytes(output: ByteArray) {
|
||||
val status = SecRandomCopyBytes(kSecRandomDefault, output.size.toULong(), output.refTo(0))
|
||||
if (status != 0) {
|
||||
// Handle error, e.g., throw an exception
|
||||
throw IllegalStateException("Failed to generate secure random bytes: $status")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import platform.Foundation.NSString
|
||||
import platform.Foundation.precomposedStringWithCompatibilityMapping
|
||||
|
||||
actual class UnicodeNormalizer {
|
||||
actual fun normalizeNFKC(input: String): String {
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
val platformString = input as NSString
|
||||
return platformString.precomposedStringWithCompatibilityMapping
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import platform.Foundation.NSURLComponents
|
||||
import platform.Foundation.NSURLQueryItem
|
||||
|
||||
actual class UriParser actual constructor(
|
||||
uri: String,
|
||||
) {
|
||||
private val nsUrlComponents: NSURLComponents = NSURLComponents(string = uri)
|
||||
|
||||
actual fun scheme(): String? = nsUrlComponents.scheme
|
||||
|
||||
actual fun host(): String? = nsUrlComponents.host
|
||||
|
||||
actual fun port(): Int? {
|
||||
// The NSNumber?.intValue is a way to handle a nullable port and convert it
|
||||
// from a platform-specific number type to a Kotlin Int.
|
||||
return nsUrlComponents.port?.intValue
|
||||
}
|
||||
|
||||
actual fun path(): String? = nsUrlComponents.path
|
||||
|
||||
actual fun queryParameterNames(): Set<String> {
|
||||
val queryItems = nsUrlComponents.queryItems ?: return emptySet()
|
||||
return queryItems.mapNotNull { (it as? NSURLQueryItem)?.name }.toSet()
|
||||
}
|
||||
|
||||
actual fun getQueryParameter(param: String): String? {
|
||||
val queryItems = nsUrlComponents.queryItems ?: return null
|
||||
return (queryItems.firstOrNull { (it as? NSURLQueryItem)?.name == param } as? NSURLQueryItem)?.value
|
||||
}
|
||||
|
||||
val fragments: Map<String, String> by lazy {
|
||||
nsUrlComponents.fragment()?.ifBlank { null }?.let { keyValuePair ->
|
||||
keyValuePair.split('&').associate { paramValue ->
|
||||
val parts = paramValue.split("=", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
parts[0] to parts[1]
|
||||
} else {
|
||||
parts[0] to "" // Handle parameters without a value, e.g., "param&other=value"
|
||||
}
|
||||
}
|
||||
} ?: emptyMap()
|
||||
}
|
||||
|
||||
actual fun fragments(): Map<String, String> = fragments
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import net.thauvin.erik.urlencoder.UrlEncoderUtil
|
||||
|
||||
actual object UrlEncoder {
|
||||
actual fun encode(value: String): String = UrlEncoderUtil.encode(value)
|
||||
|
||||
actual fun decode(value: String): String = UrlEncoderUtil.decode(value)
|
||||
}
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils.cache
|
||||
|
||||
import io.github.charlietap.cachemap.CacheMap
|
||||
import io.github.charlietap.cachemap.cacheMapOf
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.collections.plus
|
||||
import kotlin.collections.set
|
||||
|
||||
// An implementation of a Threadsafe map, using CacheMap.
|
||||
// Investigating a Swift-based alternative(for now)
|
||||
actual class LargeCache<K, V> : ICacheOperations<K, V> {
|
||||
private val concurrentMap = cacheMapOf<K, V>()
|
||||
|
||||
actual fun keys(): Set<K> = concurrentMap.keys
|
||||
|
||||
actual fun values(): Iterable<V> = concurrentMap.values
|
||||
|
||||
actual fun get(key: K): V? = concurrentMap[key]
|
||||
|
||||
actual fun remove(key: K): V? = concurrentMap.remove(key)
|
||||
|
||||
actual fun isEmpty(): Boolean = concurrentMap.isEmpty()
|
||||
|
||||
actual fun clear() {
|
||||
concurrentMap.clear()
|
||||
}
|
||||
|
||||
actual fun containsKey(key: K): Boolean = concurrentMap.containsKey(key)
|
||||
|
||||
actual fun put(
|
||||
key: K,
|
||||
value: V,
|
||||
) {
|
||||
concurrentMap.put(key, value)
|
||||
}
|
||||
|
||||
actual fun getOrCreate(
|
||||
key: K,
|
||||
builder: (K) -> V,
|
||||
): V {
|
||||
val value = concurrentMap.get(key)
|
||||
|
||||
return if (value != null) {
|
||||
value
|
||||
} else {
|
||||
val newObject = builder(key)
|
||||
concurrentMap.put(key, newObject)
|
||||
concurrentMap[key] ?: newObject
|
||||
}
|
||||
}
|
||||
|
||||
actual fun createIfAbsent(
|
||||
key: K,
|
||||
builder: (K) -> V,
|
||||
): Boolean =
|
||||
runBlocking {
|
||||
val value = concurrentMap.get(key)
|
||||
if (value != null) {
|
||||
false
|
||||
} else {
|
||||
val newObject = builder(key)
|
||||
concurrentMap.put(key, newObject)
|
||||
concurrentMap[key] != null
|
||||
}
|
||||
}
|
||||
|
||||
actual override fun size(): Int = concurrentMap.size
|
||||
|
||||
actual override fun forEach(consumer: ICacheBiConsumer<K, V>) {
|
||||
concurrentMap.forEach { consumer.accept(it.key, it.value) }
|
||||
}
|
||||
|
||||
actual override fun filter(consumer: CacheCollectors.BiFilter<K, V>): List<V> =
|
||||
concurrentMap
|
||||
.filter { consumer.filter(it.key, it.value) }
|
||||
.values
|
||||
.toList()
|
||||
|
||||
actual override fun filterIntoSet(consumer: CacheCollectors.BiFilter<K, V>): Set<V> =
|
||||
concurrentMap
|
||||
.filter { consumer.filter(it.key, it.value) }
|
||||
.values
|
||||
.toSet()
|
||||
|
||||
actual override fun <R> map(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): List<R> = concurrentMap.map { consumer.map(it.key, it.value) }
|
||||
|
||||
actual override fun <R> mapNotNull(consumer: CacheCollectors.BiMapper<K, V, R?>): List<R> = concurrentMap.mapNotNull { consumer.map(it.key, it.value) }
|
||||
|
||||
actual override fun <R> mapNotNullIntoSet(consumer: CacheCollectors.BiMapper<K, V, R?>): Set<R> = mapNotNull(consumer).toSet()
|
||||
|
||||
actual override fun <R> mapFlatten(consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>): List<R> = concurrentMap.flatMap { entry -> consumer.map(entry.key, entry.value) ?: emptyList() }
|
||||
|
||||
actual override fun <R> mapFlattenIntoSet(consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>): Set<R> = mapFlatten(consumer).toSet()
|
||||
|
||||
actual override fun maxOrNullOf(
|
||||
filter: CacheCollectors.BiFilter<K, V>,
|
||||
comparator: Comparator<V>,
|
||||
): V? {
|
||||
// return concurrentMap.maxOfWithOrNull(
|
||||
// comparator,
|
||||
// selector = {
|
||||
// if (filter.filter(it.key, it.value)) it.value else concurrentMap.getValue(it.key)
|
||||
// }
|
||||
// )
|
||||
return concurrentMap.maxOrNullOf(filter, comparator)
|
||||
}
|
||||
|
||||
actual override fun sumOf(consumer: CacheCollectors.BiSumOf<K, V>): Int {
|
||||
return concurrentMap.sumOf(consumer)
|
||||
// return concurrentMap.map { consumer.map(it.key, it.value) }.sum()
|
||||
}
|
||||
|
||||
actual override fun sumOfLong(consumer: CacheCollectors.BiSumOfLong<K, V>): Long = concurrentMap.sumOfLong(consumer)
|
||||
|
||||
actual override fun <R> groupBy(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, List<V>> = concurrentMap.groupBy(consumer)
|
||||
|
||||
actual override fun <R> countByGroup(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, Int> = concurrentMap.countByGroup(consumer)
|
||||
|
||||
actual override fun <R> sumByGroup(
|
||||
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
|
||||
): Map<R, Long> = concurrentMap.sumByGroup(groupMap, sumOf)
|
||||
|
||||
actual override fun count(consumer: CacheCollectors.BiFilter<K, V>): Int = concurrentMap.count { consumer.filter(it.key, it.value) }
|
||||
|
||||
actual override fun <T, U> associate(transform: (K, V) -> Pair<T, U>): Map<T, U> = concurrentMap.associate(transform)
|
||||
|
||||
actual override fun <U> associateWith(transform: (K, V) -> U?): Map<K, U?> = concurrentMap.associateWith(transform)
|
||||
|
||||
actual override fun filter(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiFilter<K, V>,
|
||||
): List<V> {
|
||||
val transientList = concurrentMap.subMapAlt(from, to)
|
||||
return transientList.filter { consumer.filter(it.key, it.value) }.values.toList()
|
||||
}
|
||||
|
||||
actual override fun filterIntoSet(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiFilter<K, V>,
|
||||
): Set<V> = filter(from, to, consumer).toSet()
|
||||
|
||||
actual override fun <R> map(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
): List<R> {
|
||||
val transientList = concurrentMap.subMapAlt(from, to)
|
||||
return transientList.map { consumer.map(it.key, it.value) }
|
||||
}
|
||||
|
||||
actual override fun <R> mapNotNull(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, R?>,
|
||||
): List<R> = concurrentMap.subMapAlt(from, to).mapNotNull { consumer.map(it.key, it.value) }
|
||||
|
||||
actual override fun <R> mapNotNullIntoSet(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, R?>,
|
||||
): Set<R> = mapNotNull(from, to, consumer).toSet()
|
||||
|
||||
actual override fun <R> mapFlatten(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
|
||||
): List<R> = concurrentMap.subMapAlt(from, to).flatMap { consumer.map(it.key, it.value) as Iterable<R> }
|
||||
|
||||
actual override fun <R> mapFlattenIntoSet(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
|
||||
): Set<R> = mapFlatten(from, to, consumer).toSet()
|
||||
|
||||
actual override fun maxOrNullOf(
|
||||
from: K,
|
||||
to: K,
|
||||
filter: CacheCollectors.BiFilter<K, V>,
|
||||
comparator: Comparator<V>,
|
||||
): V? {
|
||||
val transient = concurrentMap.subMapAlt(from, to)
|
||||
return transient.maxOrNullOf(filter, comparator)
|
||||
}
|
||||
|
||||
actual override fun sumOf(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiSumOf<K, V>,
|
||||
): Int = concurrentMap.subMapAlt(from, to).sumOf(consumer)
|
||||
|
||||
actual override fun sumOfLong(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiSumOfLong<K, V>,
|
||||
): Long = concurrentMap.subMapAlt(from, to).sumOfLong(consumer)
|
||||
|
||||
actual override fun <R> groupBy(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
): Map<R, List<V>> = concurrentMap.subMapAlt(from, to).groupBy(consumer)
|
||||
|
||||
actual override fun <R> countByGroup(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
): Map<R, Int> = concurrentMap.subMapAlt(from, to).countByGroup(consumer)
|
||||
|
||||
actual override fun <R> sumByGroup(
|
||||
from: K,
|
||||
to: K,
|
||||
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
|
||||
): Map<R, Long> = concurrentMap.subMapAlt(from, to).sumByGroup(groupMap, sumOf)
|
||||
|
||||
actual override fun count(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiFilter<K, V>,
|
||||
): Int = concurrentMap.subMapAlt(from, to).count { consumer.filter(it.key, it.value) }
|
||||
|
||||
actual override fun <T, U> associate(
|
||||
from: K,
|
||||
to: K,
|
||||
transform: (K, V) -> Pair<T, U>,
|
||||
): Map<T, U> = concurrentMap.subMapAlt(from, to).associate(transform)
|
||||
|
||||
actual override fun <U> associateWith(
|
||||
from: K,
|
||||
to: K,
|
||||
transform: (K, V) -> U?,
|
||||
): Map<K, U?> = concurrentMap.subMapAlt(from, to).associateWith(transform)
|
||||
|
||||
actual override fun joinToString(
|
||||
separator: CharSequence,
|
||||
prefix: CharSequence,
|
||||
postfix: CharSequence,
|
||||
limit: Int,
|
||||
truncated: CharSequence,
|
||||
transform: ((K, V) -> CharSequence)?,
|
||||
): String {
|
||||
val buffer = StringBuilder()
|
||||
buffer.append(prefix)
|
||||
var count = 0
|
||||
forEach { key, value ->
|
||||
val str = if (transform != null) transform(key, value) else ""
|
||||
if (str.isNotEmpty()) {
|
||||
if (++count > 1) buffer.append(separator)
|
||||
if (limit < 0 || count <= limit) {
|
||||
when {
|
||||
transform != null -> buffer.append(str)
|
||||
else -> buffer.append("$key $value")
|
||||
}
|
||||
} else {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
if (limit >= 0 && count > limit) buffer.append(truncated)
|
||||
buffer.append(postfix)
|
||||
return buffer.toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Different subMap implementations below. Investigating their performance for now.
|
||||
|
||||
fun <K, V> CacheMap<K, V>.subMapSlow(
|
||||
from: K,
|
||||
to: K,
|
||||
toInclusive: Boolean = true,
|
||||
): Map<K, V> {
|
||||
val transientList = toList()
|
||||
val transientSubList =
|
||||
transientList.subList(
|
||||
fromIndex = transientList.indexOf(Pair(from, getValue(from))),
|
||||
toIndex = transientList.indexOf(Pair(to, getValue(to))),
|
||||
)
|
||||
val completeSubList = transientSubList + Pair(to, getValue(to))
|
||||
|
||||
return if (toInclusive) completeSubList.toMap() else transientSubList.toMap()
|
||||
}
|
||||
|
||||
fun <K, V> CacheMap<K, V>.subMapAlt(
|
||||
from: K,
|
||||
to: K,
|
||||
toInclusive: Boolean = true,
|
||||
): Map<K, V> {
|
||||
val resultMap = hashMapOf<K, V>()
|
||||
val keySet = keys
|
||||
val fromIndex = keySet.indexOf(from)
|
||||
val toIndex = keySet.indexOf(to)
|
||||
for (index in fromIndex until toIndex) {
|
||||
val correspondingEntry = entries.elementAt(index)
|
||||
resultMap[correspondingEntry.key] = correspondingEntry.value
|
||||
}
|
||||
if (toInclusive) {
|
||||
val correspondingToEntry = entries.elementAt(toIndex)
|
||||
resultMap[correspondingToEntry.key] = correspondingToEntry.value
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
/**
|
||||
* The following functions below are (re)implementations for the ICacheOperations
|
||||
* interface. A lot of it is copying and pasting, with modifications to make it work
|
||||
* consistently.
|
||||
*/
|
||||
|
||||
fun <K, V> Map<K, V>.maxOrNullOf(
|
||||
filter: CacheCollectors.BiFilter<K, V>,
|
||||
comparator: Comparator<V>,
|
||||
): V? {
|
||||
var maxK: K? = null
|
||||
var maxV: V? = null
|
||||
forEach {
|
||||
if (filter.filter(it.key, it.value)) {
|
||||
if (maxK == null || (maxV != null && comparator.compare(it.value, maxV) > 0)) {
|
||||
maxK = it.key
|
||||
maxV = it.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val finalMaxK: K? = maxK
|
||||
val finalMaxV: V? = maxV
|
||||
|
||||
return finalMaxV
|
||||
}
|
||||
|
||||
fun <K, V> Map<K, V>.sumOf(consumer: CacheCollectors.BiSumOf<K, V>): Int {
|
||||
var sum = 0
|
||||
forEach { sum += consumer.map(it.key, it.value) }
|
||||
return sum
|
||||
}
|
||||
|
||||
fun <K, V> Map<K, V>.sumOfLong(consumer: CacheCollectors.BiSumOfLong<K, V>): Long {
|
||||
var sum = 0L
|
||||
forEach { sum += consumer.map(it.key, it.value) }
|
||||
return sum
|
||||
}
|
||||
|
||||
fun <K, V, R> Map<K, V>.groupBy(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, List<V>> {
|
||||
val results = HashMap<R, ArrayList<V>>()
|
||||
forEach {
|
||||
val group = consumer.map(it.key, it.value)
|
||||
val list = results[group]
|
||||
if (list == null) {
|
||||
val answer = ArrayList<V>()
|
||||
answer.add(it.value)
|
||||
results[group] = answer
|
||||
} else {
|
||||
list.add(it.value)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
fun <K, V, R> Map<K, V>.countByGroup(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, Int> {
|
||||
val results = HashMap<R, Int>()
|
||||
forEach {
|
||||
val group = consumer.map(it.key, it.value)
|
||||
val count = results[group]
|
||||
if (count == null) {
|
||||
results[group] = 1
|
||||
} else {
|
||||
results[group] = count + 1
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
fun <K, V, R> Map<K, V>.sumByGroup(
|
||||
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
|
||||
): Map<R, Long> {
|
||||
val results = HashMap<R, Long>()
|
||||
forEach {
|
||||
val group = groupMap.map(it.key, it.value)
|
||||
val sum = results[group]
|
||||
if (sum == null) {
|
||||
results[group] = sumOf.map(it.key, it.value)
|
||||
} else {
|
||||
results[group] = sum + sumOf.map(it.key, it.value)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
fun <K, V, T, U> Map<K, V>.associate(transform: (K, V) -> Pair<T, U>): Map<T, U> {
|
||||
val results: LinkedHashMap<T, U> = LinkedHashMap(size)
|
||||
forEach {
|
||||
val pair = transform(it.key, it.value)
|
||||
results[pair.first] = pair.second
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
fun <K, V, U> Map<K, V>.associateWith(transform: (K, V) -> U?): Map<K, U?> {
|
||||
val results: LinkedHashMap<K, U?> = LinkedHashMap(size)
|
||||
forEach {
|
||||
results[it.key] = transform(it.key, it.value)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils.ciphers
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.AES
|
||||
import dev.whyoleg.cryptography.providers.apple.Apple
|
||||
|
||||
actual class AESCBC actual constructor(
|
||||
actual val keyBytes: ByteArray,
|
||||
actual val iv: ByteArray,
|
||||
) : NostrCipher {
|
||||
private val cryptoProvider = CryptographyProvider.Apple
|
||||
private val aesCbc = cryptoProvider.get(AES.CBC)
|
||||
|
||||
private val keyDecoder =
|
||||
aesCbc
|
||||
.keyDecoder()
|
||||
.decodeFromByteArrayBlocking(format = AES.Key.Format.RAW, keyBytes)
|
||||
|
||||
private fun cipher() = keyDecoder.cipher()
|
||||
|
||||
actual override fun name(): String = "aes-cbc"
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
encryptWithIvBlocking(iv, bytesToEncrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(iv, bytesToDecrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? =
|
||||
try {
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(iv, bytesToDecrypt)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("AESCBC", "Failed to decrypt", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils.ciphers
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.AES
|
||||
|
||||
actual class AESGCM actual constructor(
|
||||
actual val keyBytes: ByteArray,
|
||||
actual val nonce: ByteArray,
|
||||
) : NostrCipher {
|
||||
private val provider = CryptographyProvider.Default
|
||||
private val aesGcm = provider.get(AES.GCM)
|
||||
|
||||
private val keyDecoder =
|
||||
aesGcm
|
||||
.keyDecoder()
|
||||
.decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes)
|
||||
|
||||
private fun cipher() = keyDecoder.cipher()
|
||||
|
||||
actual override fun name(): String = "aes-gcm"
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
encryptWithIvBlocking(nonce, bytesToEncrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(nonce, bytesToDecrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? =
|
||||
try {
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(nonce, bytesToDecrypt)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("AESGCM", "Failed to decrypt", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils.diggest
|
||||
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.RIPEMD160
|
||||
import dev.whyoleg.cryptography.algorithms.SHA1
|
||||
import dev.whyoleg.cryptography.algorithms.SHA256
|
||||
|
||||
actual class DigestInstance actual constructor(
|
||||
algorithm: String,
|
||||
) {
|
||||
private val cryptoProvider = CryptographyProvider.Default
|
||||
|
||||
private val hasher = cryptoProvider.get(digestForAlgorithm(algorithm)).hasher()
|
||||
private val hashFunction = hasher.createHashFunction()
|
||||
|
||||
actual fun update(array: ByteArray) {
|
||||
hashFunction.update(array)
|
||||
}
|
||||
|
||||
actual fun update(byte: Byte) {
|
||||
hashFunction.update(byteArrayOf(byte))
|
||||
}
|
||||
|
||||
actual fun digest(): ByteArray = hashFunction.hashToByteArray()
|
||||
|
||||
actual fun digest(input: ByteArray): ByteArray = hasher.hashBlocking(input)
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
private fun digestForAlgorithm(algorithmName: String) =
|
||||
when (algorithmName) {
|
||||
"SHA-256" -> SHA256
|
||||
|
||||
"SHA-1" -> SHA1
|
||||
|
||||
"ripemd160" -> RIPEMD160
|
||||
|
||||
// Adding this below as a reminder.
|
||||
"keccak256" -> error("KECCAK-256 is not yet supported.")
|
||||
|
||||
else -> error("This message digest is not supported.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils.mac
|
||||
|
||||
import io.github.andreypfau.kotlinx.crypto.HMac
|
||||
import io.github.andreypfau.kotlinx.crypto.Sha256
|
||||
import io.github.andreypfau.kotlinx.crypto.Sha512
|
||||
|
||||
actual class MacInstance actual constructor(
|
||||
algorithm: String,
|
||||
key: ByteArray,
|
||||
) {
|
||||
private var nativeHmac = HMac(digestForAlgorithm(algorithm), key)
|
||||
|
||||
actual fun init(
|
||||
key: ByteArray,
|
||||
algorithm: String,
|
||||
) {
|
||||
nativeHmac = HMac(digestForAlgorithm(algorithm), key)
|
||||
}
|
||||
|
||||
actual fun getMacLength(): Int = nativeHmac.macSize
|
||||
|
||||
actual fun update(array: ByteArray) {
|
||||
nativeHmac.update(array)
|
||||
}
|
||||
|
||||
actual fun update(byte: Byte) {
|
||||
nativeHmac.update(byte)
|
||||
}
|
||||
|
||||
actual fun doFinal(): ByteArray = nativeHmac.digest()
|
||||
|
||||
actual fun doFinal(
|
||||
output: ByteArray,
|
||||
offset: Int,
|
||||
) {
|
||||
nativeHmac.digest(output, offset)
|
||||
}
|
||||
|
||||
private fun digestForAlgorithm(algorithm: String) =
|
||||
when (algorithm) {
|
||||
"HmacSHA256" -> Sha256()
|
||||
"HmacSHA512" -> Sha512()
|
||||
else -> error("Algorithm is not yet supported.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils.sha256
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.convert
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.CoreCrypto.CC_SHA256
|
||||
import platform.CoreCrypto.CC_SHA256_DIGEST_LENGTH
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun sha256(data: ByteArray): ByteArray {
|
||||
val digest = UByteArray(CC_SHA256_DIGEST_LENGTH)
|
||||
|
||||
data.usePinned { inputPinned ->
|
||||
digest.usePinned { digestPinned ->
|
||||
CC_SHA256(
|
||||
inputPinned.addressOf(0),
|
||||
data.size.convert(),
|
||||
digestPinned.addressOf(0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return digest.toByteArray()
|
||||
}
|
||||
Reference in New Issue
Block a user