Merge pull request #1757 from KotlinGeekDev/kmp-completeness

KMP Completeness, Part 1.
This commit is contained in:
Vitor Pamplona
2026-03-04 10:27:24 -05:00
committed by GitHub
37 changed files with 6107 additions and 252 deletions
-4
View File
@@ -6,10 +6,6 @@
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="21" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.3" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.3.10" />
</component>
+4 -1
View File
@@ -26,4 +26,7 @@ android.enableR8.fullMode=true
kotlin.daemon.jvmargs=-Xmx12g -XX:MaxMetaspaceSize=3g
# because we use a custom jvmAndroid target
kotlin.mpp.applyDefaultHierarchyTemplate=false
kotlin.mpp.applyDefaultHierarchyTemplate=false
# This is needed for the Swift bridge to work.
kotlin.mpp.enableCInteropCommonization=true
+6
View File
@@ -1,5 +1,6 @@
[versions]
accompanistAdaptive = "0.37.3"
cachemapVersion = "0.2.4"
composeMultiplatform = "1.10.0"
activityCompose = "1.12.4"
agp = "9.1.0"
@@ -17,6 +18,7 @@ composeBom = "2026.02.00"
composeRuntimeAnnotation = "1.10.3"
coreKtx = "1.17.0"
datastore = "1.2.0"
devWhyolegCryptography = "0.5.0"
espressoCore = "3.7.0"
firebaseBom = "34.9.0"
fragmentKtx = "1.8.9"
@@ -41,6 +43,7 @@ markdown = "f92ef49c9d"
media3 = "1.9.2"
mockk = "1.14.9"
kotlinx-coroutines-test = "1.10.2"
netUrlencoderLibVersion = "1.6.0"
navigationCompose = "2.9.7"
okhttp = "5.3.2"
runner = "1.7.0"
@@ -111,11 +114,13 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
audiowaveform = { group = "com.github.lincollincol", name = "compose-audiowaveform", version.ref = "audiowaveform" }
charlietap-cachemap = { module = "io.github.charlietap:cachemap", version.ref = "cachemapVersion" }
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil-gif = { group = "io.coil-kt.coil3", name = "coil-gif", version.ref = "coil" }
coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
coil-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
coil-video = { group = "io.coil-kt.coil3", name = "coil-video", version.ref = "coil" }
dev-whyoleg-cryptography-provider-apple-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "devWhyolegCryptography" }
drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
@@ -140,6 +145,7 @@ markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", n
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
net-thauvin-erik-urlencoder-lib = { module = "net.thauvin.erik.urlencoder:urlencoder-lib", version.ref = "netUrlencoderLibVersion" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" }
rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc3986" }
+43
View File
@@ -1,11 +1,17 @@
@file:OptIn(ExperimentalSpmForKmpFeature::class)
import com.vanniktech.maven.publish.KotlinMultiplatform
import io.github.frankois944.spmForKmp.swiftPackageConfig
import io.github.frankois944.spmForKmp.utils.ExperimentalSpmForKmpFeature
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKotlinMultiplatformLibrary)
alias(libs.plugins.serialization)
alias(libs.plugins.vanniktech.mavenPublish)
id("io.github.frankois944.spmForKmp") version "1.4.8"
}
kotlin {
@@ -56,6 +62,28 @@ kotlin {
// https://developer.android.com/kotlin/multiplatform/migrate
val xcfName = "quartz-kmpKit"
listOf(
iosArm64(),
iosX64(),
iosSimulatorArm64()
).forEach { target ->
target.swiftPackageConfig(cinteropName = "swiftbridge") {
minIos = "17"
minMacos = "14"
dependency {
remotePackageVersion(
url = uri("https://github.com/swift-standards/swift-rfc-3986.git"),
packageName = "swift-rfc-3986",
products = {
add("RFC 3986")
},
version = "0.1.0"
)
}
}
}
iosX64 {
binaries.framework {
baseName = xcfName
@@ -73,6 +101,18 @@ kotlin {
baseName = xcfName
}
}
//This makes sure that the resource file directory is visible for iOS tests.
val rootDir = "${rootProject.rootDir.path}/quartz/src/iosTest/resources"
tasks.withType<Test>().configureEach {
environment("TEST_RESOURCES_ROOT", rootDir)
}
tasks.withType<KotlinNativeTest>().configureEach {
environment("TEST_RESOURCES_ROOT", rootDir)
// This is necessary to have the variable propagated on iOS
environment("SIMCTL_CHILD_TEST_RESOURCES_ROOT", rootDir)
}
// Source set declarations.
// Declaring a target automatically creates a source set with the same name. By default, the
@@ -199,6 +239,9 @@ kotlin {
iosMain {
dependsOn(commonMain.get())
dependencies {
implementation(libs.charlietap.cachemap)
implementation(libs.net.thauvin.erik.urlencoder.lib)
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
}
}
@@ -20,8 +20,11 @@
*/
package com.vitorpamplona.quartz
actual class TestResourceLoader actual constructor() {
actual fun loadString(file: String): String {
TODO("Not yet implemented")
}
actual class TestResourceLoader {
actual fun loadString(file: String): String =
this@TestResourceLoader
.javaClass.classLoader
?.getResourceAsStream(file)
?.bufferedReader()
?.use { it.readText() } ?: throw IllegalArgumentException("Resource not found: $file")
}
File diff suppressed because it is too large Load Diff
@@ -76,8 +76,8 @@ object SCrypt {
): ByteArray {
require(!(n < 2 || (n and (n - 1)) != 0)) { "N must be a power of 2 greater than 1" }
require(n <= Int.Companion.MAX_VALUE / 128 / r) { "Parameter N is too large" }
require(r <= Int.Companion.MAX_VALUE / 128 / p) { "Parameter r is too large" }
require(n <= Int.MAX_VALUE / 128 / r) { "Parameter N is too large" }
require(r <= Int.MAX_VALUE / 128 / p) { "Parameter r is too large" }
val mac = MacInstance("HmacSHA256", passwd)
@@ -0,0 +1,44 @@
/*
* 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.nip10Notes.urls
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
import com.vitorpamplona.quartz.utils.fastFindURLs
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
class UrlsDetectorTest {
private val testSentence = "I have a website at https://mysite.xyz and a blog at https://myblog.xyz"
@Test
fun detectUrlNumber() {
val detectedLinks = fastFindURLs(testSentence)
assertEquals(2, detectedLinks.size)
}
@Test
fun urlsAreCorrect() {
val detectedLinks = findURLs(testSentence)
assertContains(detectedLinks, "https://mysite.xyz")
assertContains(detectedLinks, "https://myblog.xyz")
}
}
@@ -0,0 +1,81 @@
/*
* 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 nip06KeyDerivationCommon
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip06KeyDerivation.Bip32SeedDerivation
import com.vitorpamplona.quartz.nip06KeyDerivation.Bip39Mnemonics
import com.vitorpamplona.quartz.nip06KeyDerivation.KeyPath
import kotlin.test.Test
import kotlin.test.assertEquals
class Bip32SeedDerivationCommonTest {
val seedDerivation = Bip32SeedDerivation()
val masterBitcoin =
seedDerivation.generate(
Bip39Mnemonics.toSeed("gun please vital unable phone catalog explain raise erosion zoo truly exist", ""),
)
val nostrMnemonic0 =
seedDerivation.generate(
Bip39Mnemonics.toSeed("leader monkey parrot ring guide accident before fence cannon height naive bean", ""),
)
val nostrMnemonic1 =
seedDerivation.generate(
Bip39Mnemonics.toSeed("what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade", ""),
)
@Test
fun restoreBIP44Wallet() {
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/44'/1'/0'"))
assertEquals("50b3e7905c642309c8a8b73df5a49757a10f2bebb5804571b9db9004cce8a190", privateKey.toHexKey())
}
@Test
fun restoreBIP49Wallet() {
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/49'/1'/0'"))
assertEquals("154c02c0b66899291a19012207642ba096a2d3ebf51baf153c9495976feb1b30", privateKey.toHexKey())
}
@Test
fun restoreBIP84Wallet() {
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/84'/1'/0'"))
assertEquals("53e8c09a0e3ddcd8d68821c1e99e823966e99df91fb253e1f453a443ba543cb2", privateKey.toHexKey())
}
@Test
fun testGenerateMasterKeyForNostrMnemonics0() {
assertEquals(
"dbbcc0e112894d1430d5bc348d1bd72e8ac339952702be1fe572de80fe1b7fcb",
nostrMnemonic0.secretkeybytes.toHexKey(),
)
}
@Test
fun testGenerateMasterKeyForNostrMnemonics1() {
assertEquals(
"d58d40d5724435552fa442350b75e0ff95a19d990e908e3a516bcc88f780108f",
nostrMnemonic1.secretkeybytes.toHexKey(),
)
}
}
@@ -0,0 +1,68 @@
/*
* 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 nip06KeyDerivationCommon
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
class Nip06CommonTest {
val nip06 = Nip06()
// private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a
// nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp
private val menemonic0 = "leader monkey parrot ring guide accident before fence cannon height naive bean"
// private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add
// nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel
private val menemonic1 = "what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade"
// private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add
// nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel
private val snortTest = "clog remember sample endorse mountain key rib hurry question supreme palm future stage style swing faith erase thumb then warm truth mule vivid endless"
@Test
fun fromSeedNip06TestVector0() {
val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic0).toHexKey()
assertEquals("7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a", privateKeyHex)
val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic0, 21).toHexKey()
assertEquals("576390ec69951fcfbf159f2aac0965bb2e6d7a07da2334992af3225c57eaefca", privateKeyHex21)
}
@Test
fun fromSeedNip06TestVector1() {
val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic1).toHexKey()
assertEquals("c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add", privateKeyHex)
val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic1, 42).toHexKey()
assertEquals("ad993054383da74e955f8b86346365b5ffd6575992e1de3738dda9f94407052b", privateKeyHex21)
}
@Test
@Ignore()
fun fromSeedNip06FromSnort() {
val privateKeyNsec = nip06.privateKeyFromMnemonic(snortTest).toHexKey()
assertEquals("nsec1ppw9ltr2x9qwg9a2qnmgv98tfruy2ejnja7me76mwmsreu3s8u2sscj5nt", privateKeyNsec)
}
}
@@ -21,17 +21,46 @@
package com.vitorpamplona.quartz.nip01Core.crypto
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.fastForEach
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.sha256.sha256
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonArray
import kotlinx.serialization.json.buildJsonArray
actual object EventHasherSerializer {
fun makeJsonObjectForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): JsonArray {
val arrayBuilder =
buildJsonArray {
add(0)
add(pubKey)
add(createdAt)
add(kind)
addJsonArray {
tags.fastForEach { tag ->
// add(JsonArray(content = tag.map { JsonPrimitive(it) }))
addJsonArray { tag.fastForEach { add(it) } }
}
}
add(content)
}
return arrayBuilder
}
actual fun makeJsonForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): String {
TODO("Not yet implemented")
}
): String = makeJsonObjectForId(pubKey, createdAt, kind, tags, content).toString()
actual fun fastMakeJsonForId(
pubKey: HexKey,
@@ -40,7 +69,8 @@ actual object EventHasherSerializer {
tags: Array<Array<String>>,
content: String,
): ByteArray {
TODO("Not yet implemented")
// Also use makeJsonObjectForId(...) above. May change this if needed(for performance).
return makeJsonObjectForId(pubKey, createdAt, kind, tags, content).toString().encodeToByteArray()
}
actual fun makeJsonForIdHashAndCheck(
@@ -51,6 +81,8 @@ actual object EventHasherSerializer {
tags: Array<Array<String>>,
content: String,
): Boolean {
TODO("Not yet implemented")
// Also use makeJsonObjectForId(...) above. May change byteArray encode method if needed.
val eventIdHash = sha256(makeJsonObjectForId(pubKey, createdAt, kind, tags, content).toString().encodeToByteArray())
return Hex.isEqual(id, eventIdHash)
}
}
@@ -20,9 +20,29 @@
*/
package com.vitorpamplona.quartz.nip96FileStorage.info
import platform.Foundation.NSURL
import platform.Foundation.NSURLComponents
actual fun makeAbsoluteIfRelativeUrl(
baseUrl: String,
potentiallyRelativeUrl: String,
): String {
TODO("Not yet implemented")
}
): 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
}
@@ -20,32 +20,32 @@
*/
package com.vitorpamplona.quartz.utils
import com.vitorpamplona.quartz.utils.io.CustomBitSet
actual class BitSet {
val nativeBitSet: CustomBitSet
actual constructor(nBits: Int) {
TODO("Not yet implemented")
nativeBitSet = CustomBitSet(nBits)
}
constructor(bytes: ByteArray) {
nativeBitSet = CustomBitSet(bytes)
}
actual fun set(bitIndex: Int) {
TODO("Not yet implemented")
nativeBitSet.set(bitIndex, true)
}
actual fun clear(bitIndex: Int) {
TODO("Not yet implemented")
nativeBitSet.clear(bitIndex)
}
actual fun get(bitIndex: Int): Boolean {
TODO("Not yet implemented")
}
actual fun get(bitIndex: Int): Boolean = nativeBitSet[bitIndex]
actual fun size(): Int {
TODO("Not yet implemented")
}
actual fun size(): Int = nativeBitSet.size()
actual fun toByteArray(): ByteArray {
TODO("Not yet implemented")
}
actual fun toByteArray(): ByteArray = nativeBitSet.toByteArray()
}
actual fun bitSetValueOf(bytes: ByteArray): BitSet {
TODO("Not yet implemented")
}
actual fun bitSetValueOf(bytes: ByteArray): BitSet = BitSet(bytes)
@@ -20,12 +20,36 @@
*/
package com.vitorpamplona.quartz.utils
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSURLComponents
import swiftbridge.Rfc3986UriBridge
@OptIn(ExperimentalForeignApi::class)
actual object Rfc3986 {
actual fun normalize(uri: String): String = TODO()
private val rfc3986UriBridge = Rfc3986UriBridge()
actual fun isValidUrl(url: String): Boolean = TODO()
actual fun normalize(uri: String): String =
rfc3986UriBridge
.normalizeUrlWithUrl(uri, null)
?.let { if (it.last() == '/') it else "$it/" } ?: throw Exception("Could not normalize URI: $uri")
actual fun normalizeAndRemoveFragment(url: String): String = TODO()
actual fun isValidUrl(url: String): Boolean = rfc3986UriBridge.isUrlValidWithUrl(url)
actual fun host(url: String): String = TODO()
actual fun normalizeAndRemoveFragment(url: String): String =
NSURLComponents(url)
.toStringNoFragment()
.internIfPossible()
actual fun host(url: String): String = rfc3986UriBridge.hostFromUriWithUrl(url, null) ?: throw Exception("Could not retrieve host from URL.")
}
fun NSURLComponents.toStringNoFragment(): String {
val sb = StringBuilder()
if (scheme != null) sb.append(scheme).append(":")
if (host != null) sb.append("//").append(host.toString())
if (path != null) sb.append(path)
if (query != null) sb.append("?").append(query)
return sb.toString()
}
@@ -20,49 +20,40 @@
*/
package com.vitorpamplona.quartz.utils
actual object Secp256k1Instance {
actual fun compressedPubKeyFor(privKey: ByteArray): ByteArray {
TODO("Not yet implemented")
}
import fr.acinq.secp256k1.Secp256k1
actual fun isPrivateKeyValid(il: ByteArray): Boolean {
TODO("Not yet implemented")
}
actual object Secp256k1Instance {
private val h02 = Hex.decode("02")
private val secp256k1Ref = Secp256k1.get()
actual fun compressedPubKeyFor(privKey: ByteArray): ByteArray = secp256k1Ref.pubKeyCompress(secp256k1Ref.pubkeyCreate(privKey))
actual fun isPrivateKeyValid(il: ByteArray): Boolean = secp256k1Ref.secKeyVerify(il)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray?,
): ByteArray {
TODO("Not yet implemented")
}
): ByteArray = secp256k1Ref.signSchnorr(data, privKey, nonce)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
): ByteArray {
TODO("Not yet implemented")
}
): ByteArray = secp256k1Ref.signSchnorr(data, privKey, null)
actual fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean {
TODO("Not yet implemented")
}
): Boolean = secp256k1Ref.verifySchnorr(signature, hash, pubKey)
actual fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray {
TODO("Not yet implemented")
}
): ByteArray = secp256k1Ref.privKeyTweakAdd(first, second)
actual fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray {
TODO("Not yet implemented")
}
): ByteArray = secp256k1Ref.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
}
@@ -20,6 +20,12 @@
*/
package com.vitorpamplona.quartz.utils
import kotlinx.cinterop.ExperimentalForeignApi
import swiftbridge.UrlDetector
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalForeignApi::class)
actual fun fastFindURLs(text: String): List<String> {
TODO("Not yet implemented")
val detectorInstance = UrlDetector()
return detectorInstance.findURLsWithText(text) as List<String>
}
@@ -20,8 +20,13 @@
*/
package com.vitorpamplona.quartz.utils
import platform.Foundation.NSString
import platform.Foundation.precomposedStringWithCompatibilityMapping
actual class UnicodeNormalizer {
actual fun normalizeNFKC(input: String): String {
TODO("Not yet implemented")
@Suppress("CAST_NEVER_SUCCEEDS")
val platformString = input as NSString
return platformString.precomposedStringWithCompatibilityMapping
}
}
@@ -20,12 +20,10 @@
*/
package com.vitorpamplona.quartz.utils
actual object UrlEncoder {
actual fun encode(value: String): String {
TODO("Not yet implemented")
}
import net.thauvin.erik.urlencoder.UrlEncoderUtil
actual fun decode(value: String): String {
TODO("Not yet implemented")
}
actual object UrlEncoder {
actual fun encode(value: String): String = UrlEncoderUtil.encode(value)
actual fun decode(value: String): String = UrlEncoderUtil.decode(value)
}
@@ -20,188 +20,180 @@
*/
package com.vitorpamplona.quartz.utils.cache
// Need of find a Concurrent/ThreadSafe HashMap in iOS
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> {
actual fun keys(): Set<K> {
TODO("Not yet implemented")
}
private val concurrentMap = cacheMapOf<K, V>()
actual fun values(): Iterable<V> {
TODO("Not yet implemented")
}
actual fun keys(): Set<K> = concurrentMap.keys
actual fun get(key: K): V? {
TODO("Not yet implemented")
}
actual fun values(): Iterable<V> = concurrentMap.values
actual fun remove(key: K): V? {
TODO("Not yet implemented")
}
actual fun get(key: K): V? = concurrentMap[key]
actual fun isEmpty(): Boolean {
TODO("Not yet implemented")
}
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 {
TODO("Not yet implemented")
}
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 {
TODO("Not yet implemented")
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 {
TODO("Not yet implemented")
}
): 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 {
TODO("Not yet implemented")
}
actual override fun size(): Int = concurrentMap.size
actual override fun forEach(consumer: ICacheBiConsumer<K, V>) {
TODO("Not yet implemented")
concurrentMap.forEach { consumer.accept(it.key, it.value) }
}
actual override fun filter(consumer: CacheCollectors.BiFilter<K, V>): List<V> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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? {
TODO("Not yet implemented")
// 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 {
TODO("Not yet implemented")
return concurrentMap.sumOf(consumer)
// return concurrentMap.map { consumer.map(it.key, it.value) }.sum()
}
actual override fun sumOfLong(consumer: CacheCollectors.BiSumOfLong<K, V>): Long {
TODO("Not yet implemented")
}
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>> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
): Map<R, Long> = concurrentMap.sumByGroup(groupMap, sumOf)
actual override fun count(consumer: CacheCollectors.BiFilter<K, V>): Int {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
}
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?> {
TODO("Not yet implemented")
}
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> {
TODO("Not yet implemented")
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> {
TODO("Not yet implemented")
}
): Set<V> = filter(from, to, consumer).toSet()
actual override fun <R> map(
from: K,
to: K,
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
): List<R> {
TODO("Not yet implemented")
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> {
TODO("Not yet implemented")
}
): 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> {
TODO("Not yet implemented")
}
): 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> {
TODO("Not yet implemented")
}
): 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> {
TODO("Not yet implemented")
}
): Set<R> = mapFlatten(from, to, consumer).toSet()
actual override fun maxOrNullOf(
from: K,
@@ -209,73 +201,58 @@ actual class LargeCache<K, V> : ICacheOperations<K, V> {
filter: CacheCollectors.BiFilter<K, V>,
comparator: Comparator<V>,
): V? {
TODO("Not yet implemented")
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 {
TODO("Not yet implemented")
}
): Int = concurrentMap.subMapAlt(from, to).sumOf(consumer)
actual override fun sumOfLong(
from: K,
to: K,
consumer: CacheCollectors.BiSumOfLong<K, V>,
): Long {
TODO("Not yet implemented")
}
): 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>> {
TODO("Not yet implemented")
}
): 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> {
TODO("Not yet implemented")
}
): 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> {
TODO("Not yet implemented")
}
): Map<R, Long> = concurrentMap.subMapAlt(from, to).sumByGroup(groupMap, sumOf)
actual override fun count(
from: K,
to: K,
consumer: CacheCollectors.BiFilter<K, V>,
): Int {
TODO("Not yet implemented")
}
): 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> {
TODO("Not yet implemented")
}
): 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?> {
TODO("Not yet implemented")
}
): Map<K, U?> = concurrentMap.subMapAlt(from, to).associateWith(transform)
actual override fun joinToString(
separator: CharSequence,
@@ -285,6 +262,172 @@ actual class LargeCache<K, V> : ICacheOperations<K, V> {
truncated: CharSequence,
transform: ((K, V) -> CharSequence)?,
): String {
TODO("Not yet implemented")
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
}
@@ -20,23 +20,48 @@
*/
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 {
actual override fun name(): String {
TODO("Not yet implemented")
}
private val cryptoProvider = CryptographyProvider.Apple
private val aesCbc = cryptoProvider.get(AES.CBC)
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray {
TODO("Not yet implemented")
}
private val keyDecoder =
aesCbc
.keyDecoder()
.decodeFromByteArrayBlocking(format = AES.Key.Format.RAW, keyBytes)
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray {
TODO("Not yet implemented")
}
private fun cipher() = keyDecoder.cipher()
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? {
TODO("Not yet implemented")
}
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
}
}
@@ -20,23 +20,47 @@
*/
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 {
actual override fun name(): String {
TODO("Not yet implemented")
}
private val provider = CryptographyProvider.Default
private val aesGcm = provider.get(AES.GCM)
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray {
TODO("Not yet implemented")
}
private val keyDecoder =
aesGcm
.keyDecoder()
.decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes)
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray {
TODO("Not yet implemented")
}
private fun cipher() = keyDecoder.cipher()
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? {
TODO("Not yet implemented")
}
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
}
}
@@ -20,22 +20,44 @@
*/
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) {
TODO("Not yet implemented")
hashFunction.update(array)
}
actual fun update(byte: Byte) {
TODO("Not yet implemented")
hashFunction.update(byteArrayOf(byte))
}
actual fun digest(): ByteArray {
TODO("Not yet implemented")
}
actual fun digest(): ByteArray = hashFunction.hashToByteArray()
actual fun digest(input: ByteArray): ByteArray {
TODO("Not yet implemented")
}
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,685 @@
/*
* 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.io
// Credits: skolson, from https://github.com/skolson/KmpIO
@Suppress("ktlint")
abstract class Buffer<Element, Array> internal constructor(
markPosition: Int,
pos: Int,
lim: Int,
cap: Int,
var order: ByteOrder = ByteOrder.LittleEndian,
) {
enum class ByteOrder { LittleEndian, BigEndian }
// Invariants: mark <= position <= limit <= capacity
var position = pos
/**
* Sets this buffer's position. If the mark is defined and larger than the
* new position then it is discarded.
*
* @param newPosition
* The new position value; must be non-negative
* and no larger than the current limit
*
* @throws IllegalArgumentException
* If the preconditions on <tt>newPosition</tt> do not hold
*/
set(newPosition) {
if (newPosition !in 0..limit) {
throw IllegalArgumentException("Position $newPosition exceeds limit:$limit")
}
field = newPosition
if (mark > position) mark = NO_MARK
}
var limit = lim
/**
* Sets this buffer's limit. If the position is larger than the new limit
* then it is set to the new limit. If the mark is defined and larger than
* the new limit then it is discarded.
*
* @param newLimit
* The new limit value; must be non-negative
* and no larger than this buffer's capacity
*
* @return This buffer
*
* @throws IllegalArgumentException
* If the preconditions on <tt>newLimit</tt> do not hold
*/
set(newLimit) {
if (newLimit !in 0..capacity) {
throw IllegalArgumentException("limit $newLimit exceeds capacity $capacity")
}
field = newLimit
if (position > limit) position = limit
if (mark > limit) mark = -1
}
var capacity = cap
internal set
var mark = markPosition
set(value) {
if (mark < -1 || mark > position) {
throw IllegalArgumentException("Mark $mark out of range of 0 to $position")
}
field = value
}
/**
* Returns the number of elements between the current position and the
* limit.
*
* @return The number of elements remaining in this buffer
*/
val remaining get() = limit - position
/**
* Tells whether there are any elements between the current position and
* the limit.
*
* @return <tt>true</tt> if, and only if, there is at least one element
* remaining in this buffer
*/
val hasRemaining get() = position < limit
/**
* Tells whether or not this buffer is read-only.
*
* @return <tt>true</tt> if, and only if, this buffer is read-only
*/
abstract val isReadOnly: Boolean
/**
* Subclasses provide a property that get a byte from the buffer, or put a byte in the buffer.
* Position must be incremented in both cases
*/
abstract var byte: Element
// The following functions are all usable by subclasses to encode/decode basic types
// in and Endian-aware fashion. In all cases an exception is thrown if remaining() < length
// to be read/written. Position will be incremented by the appropriate length for both gets
// and sets.
//
// For some reason, the ByteArray in Kotlin Native is the only implementation that provides
// similar function, but assumes all is Little Endian. Since these are not available in the
// common stdlib, they are not used even for little endian.
/**
* Read or write a two-byte Char at the current position. Position will increment by 2.
*/
var char: Char
get() {
val c =
when (order) {
ByteOrder.LittleEndian ->
(
(getElementAsInt(position + 1) shl 8) or
getElementAsInt(position)
).toChar()
ByteOrder.BigEndian ->
(
(getElementAsInt(position) shl 8) or
getElementAsInt(position + 1)
).toChar()
}
position += 2
return c
}
set(value) {
putEndian(shortToArray(value.code.toShort()))
}
/**
* Read or write a two-byte Short at the current position. Position will increment by 2.
*/
var short: Short
get() {
if (remaining < SHORT_LENGTH) {
throw IllegalArgumentException("Short requires $SHORT_LENGTH bytes. Position: $position, remaining:$remaining")
}
val s = getShortValue(position)
position += SHORT_LENGTH
return s
}
set(value) {
putEndian(shortToArray(value))
}
/**
* Read or write a two-byte Short at the current position. Position will increment by 2.
*/
var ushort: UShort
get() {
if (remaining < SHORT_LENGTH) {
throw IllegalArgumentException("UShort requires $SHORT_LENGTH bytes. Position: $position, remaining:$remaining")
}
val s = getUShortValue(position)
position += SHORT_LENGTH
return s
}
set(value) {
putEndian(ushortToArray(value))
}
/**
* Read or write a four-byte Int at the current position. Position will increment by 4.
*/
var int: Int
get() {
if (remaining < INT_LENGTH) {
throw IllegalArgumentException("Int requires $INT_LENGTH bytes. Position: $position, remaining:$remaining")
}
val s = getIntValue(position)
position += INT_LENGTH
return s
}
set(value) {
putEndian(intToArray(value))
}
/**
* Read or write a two-byte Short at the current position. Position will increment by 2.
*/
var uint: UInt
get() {
if (remaining < INT_LENGTH) {
throw IllegalArgumentException("UInt requires $INT_LENGTH bytes. Position: $position, remaining:$remaining")
}
val s = getUIntValue(position)
position += INT_LENGTH
return s
}
set(value) {
putEndian(uintToArray(value))
}
/**
* Read or write a four-byte Int at the current position. Position will increment by 4.
*/
var long: Long
get() {
if (remaining < LONG_LENGTH) {
throw IllegalArgumentException("Long requires $LONG_LENGTH bytes. Position: $position, remaining:$remaining")
}
val s = getLongValue(position)
position += LONG_LENGTH
return s
}
set(value) {
putEndian(longToArray(value))
}
/**
* Read or write a four-byte Int at the current position. Position will increment by 4.
*/
var ulong: ULong
get() {
if (remaining < LONG_LENGTH) {
throw IllegalArgumentException("ULong requires $LONG_LENGTH bytes. Position: $position, remaining:$remaining")
}
val s = getULongValue(position)
position += LONG_LENGTH
return s
}
set(value) {
putEndian(ulongToArray(value))
}
var float: Float
get() = Float.fromBits(this.int)
set(value) {
int = value.toBits()
}
var double: Double
get() = Double.Companion.fromBits(this.long)
set(value) {
long = value.toBits()
}
// Creates a new buffer with the given mark, position, limit, and capacity,
// after checking invariants.
init {
if (cap < 0) throw IllegalArgumentException("Negative capacity: $cap")
capacity = cap
limit = lim
position = pos
if (mark >= 0) {
if (mark > pos) {
throw IllegalArgumentException("mark > position: ($mark > $pos)")
}
this.mark = mark
}
}
/**
* Clears this buffer. The position is set to zero, the limit is set to
* the capacity, and the mark is discarded.
*
*
* Invoke this method before using a sequence of channel-read or
* *put* operations to fill this buffer. For example:
*
* <blockquote><pre>
* buf.clear(); // Prepare buffer for reading
* in.read(buf); // Read data</pre></blockquote>
*
*
* This method does not actually erase the data in the buffer, but it
* is named as if it did because it will most often be used in situations
* in which that might as well be the case.
*
* @return This buffer
*/
open fun clear() {
position = 0
limit = capacity
mark = NO_MARK
}
/**
* Increase size of buffer. Capacity and content are increased. Position is unchanged. if limit
* currently set to capacity, it will be set to the new capacity
* @param addCapacity bytes to add. Unsigned as can't be used to shrink.
*/
abstract fun expand(addCapacity: UInt)
/**
* Flips this buffer. The limit is set to the current position and then
* the position is set to zero. If the mark is defined then it is
* discarded.
*
*
* After a sequence of channel-read or *put* operations, invoke
* this method to prepare for a sequence of channel-write or relative
* *get* operations. For example:
*
* <blockquote><pre>
* buf.put(magic); // Prepend header
* in.read(buf); // Read data into rest of buffer
* buf.flip(); // Flip buffer
* out.write(buf); // Write header + data to channel</pre></blockquote>
*
*
* This method is often used in conjunction with the compact method when transferring data from
* one place to another.
*
* @return This buffer
*/
open fun flip(): Buffer<Element, Array> {
limit = position
position = 0
mark = NO_MARK
return this
}
/**
* given the specified index into the backing array, return the current Element. Do not change position
*/
fun get(index: Int): Element = getElementAt(index)
/**
* This is used by many of the accessor properties of the various types. It must return
* a ByteArray of length bytes, containing the bytes at the current position for the
* specified length. The position will be incremented by the length.
*
* @param length must be > 0 and <= remaining(). Defaults to remaining
*
* @throws IllegalArgumentException if the length is invalid
*/
abstract fun getBytes(length: Int = remaining): Array
/**
* Starting at the current position, copy bytes.size from the buffer into the provided array.
* If the size of the array is greater than [remaining], only [remaining] bytes are copied.
* Position is increased by the number of bytes copied.
*/
abstract fun getBytes(bytes: Array)
/**
* given the specified index into the backing array, return the current Element. Do not change position
*/
abstract fun getElementAt(index: Int): Element
abstract fun setElementAt(
index: Int,
element: Element,
)
abstract fun getElementAsInt(index: Int): Int
abstract fun getElementAsUInt(index: Int): UInt
abstract fun getElementAsLong(index: Int): Long
abstract fun getElementAsULong(index: Int): ULong
/**
* Convenience method Sets the limit at position + length, then sets the position
*/
fun positionLimit(
position: Int,
length: Int,
) {
if (length < 0 || position < 0 || position + length > capacity) {
throw IllegalArgumentException("Position: $position + length: $length = ${position + length} must be between 0 and $capacity")
}
limit = position + length
this.position = position
}
fun positionLimit(
position: Short,
length: Short,
) {
if (length < 0 || position < 0 || position + length > capacity) {
throw IllegalArgumentException("Position: $position + length: $length = ${position + length} must be between 0 and $capacity")
}
limit = position + length
this.position = position.toInt()
}
/**
* This is used by many of the accessor properties of the various types. It must copy the
* content of bytes into the ByteBuffer at the current position.
* The position will be incremented by the length of the array.
*
* @param bytes size of the array must be > 0 and <= remaining()
*
* @throws IllegalArgumentException if the size is invalid
*/
abstract fun put(bytes: Array)
/**
* Resets this buffer's position to the previously-marked position.
*
*
* Invoking this method neither changes nor discards the mark's
* value.
*
* @return This buffer
*
* @throws IllegalStateException
* If the mark has not been set
*/
open fun reset() {
val m = mark
if (m < 0) throw IllegalStateException("Mark:$m must be non-negative")
position = m
}
fun resetMark() {
mark = NO_MARK
}
/**
* Rewinds this buffer. The position is set to zero and the mark is
* discarded.
*
*
* Invoke this method before a sequence of channel-write or *get*
* operations, assuming that the limit has already been set
* appropriately. For example:
*
* <blockquote><pre>
* out.write(buf); // Write remaining data
* buf.rewind(); // Rewind buffer
* buf.get(array); // Copy data into array</pre></blockquote>
*
* @return This buffer
*/
open fun rewind() {
position = 0
mark = -1
}
/**
* Make a new ByteBuffer containing the [remaining bytes] of this one. Length can be overrdiden to
* a shorter value than the default [remaining]. Position is unaffected
* @param length defaults to [remaining]. can be between 1 and [remaining]
*/
abstract fun slice(length: Int = remaining): ByteBufferBase<Element, Array>
// -- Package-private methods for bounds checking, etc. --
/**
* Checks the current position against the limit, throwing a [ ] if it is not smaller than the limit, and then
* increments the position.
*
* @return The current position value, before it is incremented
*/
fun nextGetIndex(): Int {
if (position >= limit) throw IllegalStateException("Position:$position exceeds Limit:$limit")
return position++
}
fun nextGetIndex(nb: Int): Int {
if (limit - position < nb) throw IllegalStateException()
val p = position
position += nb
return p
}
/**
* Checks the current position against the limit, and then increments the position.
*
* @return The current position value, before it is incremented
* @throws IllegalStateException if position exceeds limit
*/
fun nextPutIndex(): Int {
if (position >= limit) throw IllegalStateException("Position:$position exceeds Limit:$limit")
return position++
}
/**
* Checks the current position pluss the specified offset against the limit, and then increments the position.
*
* @param length position will be increased by
* @return The current position value, before it is incremented
* @throws IllegalStateException if increased position will exceed limit
*/
fun nextPutIndex(length: Int): Int {
if (limit - position < length || position < 0) {
throw IllegalStateException("Limit:$limit minus position:$position less than $length")
}
val p = position
position += length
return p
}
/**
* Checks the given index against the limit, throwing an [ ] if it is not smaller than the limit
* or is smaller than zero.
*/
fun checkIndex(i: Int): Int { // package-private
if (i < 0 || i >= limit) {
throw IndexOutOfBoundsException(
"index=$i out of bounds (limit=$limit)",
)
}
return i
}
fun checkIndex(
i: Int,
nb: Int,
): Int { // package-private
if (i < 0 || nb > limit - i) {
throw IndexOutOfBoundsException(
"index=$i out of bounds (limit=$limit, nb=$nb)",
)
}
return i
}
fun markValue(): Int { // package-private
return mark
}
fun truncate() { // package-private
mark = NO_MARK
position = 0
limit = 0
capacity = 0
}
internal fun discardMark() { // package-private
mark = -1
}
private fun getShortValue(index: Int): Short =
when (order) {
ByteOrder.LittleEndian -> ((getElementAsInt(index + 1) shl 8) or getElementAsInt(index)).toShort()
ByteOrder.BigEndian -> ((getElementAsInt(index) shl 8) or getElementAsInt(index + 1)).toShort()
}
fun getUShortValue(index: Int): UShort =
when (order) {
ByteOrder.LittleEndian ->
(
(getElementAsUInt(index + 1) shl 8) or
getElementAsUInt(index)
).toUShort()
ByteOrder.BigEndian ->
(
(getElementAsUInt(index) shl 8) or
getElementAsUInt(index + 1)
).toUShort()
}
private fun getIntValue(index: Int): Int =
when (order) {
ByteOrder.LittleEndian ->
(getElementAsInt(index + 3) shl 24) or
(getElementAsInt(index + 2) shl 16) or
(getElementAsInt(index + 1) shl 8) or
getElementAsInt(index)
ByteOrder.BigEndian ->
(getElementAsInt(index) shl 24) or
(getElementAsInt(index + 1) shl 16) or
(getElementAsInt(index + 2) shl 8) or
getElementAsInt(index + 3)
}
private fun getUIntValue(index: Int): UInt =
when (order) {
ByteOrder.LittleEndian ->
(getElementAsUInt(index + 3) shl 24) or
(getElementAsUInt(index + 2) shl 16) or
(getElementAsUInt(index + 1) shl 8) or
getElementAsUInt(index)
ByteOrder.BigEndian ->
(getElementAsUInt(index) shl 24) or
(getElementAsUInt(index + 1) shl 16) or
(getElementAsUInt(index + 2) shl 8) or
getElementAsUInt(index + 3)
}
fun getLongValue(index: Int): Long =
when (order) {
ByteOrder.LittleEndian -> (
(getElementAsLong(index + 7) shl 56)
or (getElementAsLong(index + 6) shl 48)
or (getElementAsLong(index + 5) shl 40)
or (getElementAsLong(index + 4) shl 32)
or (getElementAsLong(index + 3) shl 24)
or (getElementAsLong(index + 2) shl 16)
or (getElementAsLong(index + 1) shl 8)
or getElementAsLong(index)
)
ByteOrder.BigEndian -> (
(getElementAsLong(index) shl 56)
or (getElementAsLong(index + 1) shl 48)
or (getElementAsLong(index + 2) shl 40)
or (getElementAsLong(index + 3) shl 32)
or (getElementAsLong(index + 4) shl 24)
or (getElementAsLong(index + 5) shl 16)
or (getElementAsLong(index + 6) shl 8)
or getElementAsLong(index + 7)
)
}
private fun getULongValue(index: Int): ULong =
when (order) {
ByteOrder.LittleEndian -> (
(getElementAsULong(index + 7) shl 56)
or (getElementAsULong(index + 6) shl 48)
or (getElementAsULong(index + 5) shl 40)
or (getElementAsULong(index + 4) shl 32)
or (getElementAsULong(index + 3) shl 24)
or (getElementAsULong(index + 2) shl 16)
or (getElementAsULong(index + 1) shl 8)
or getElementAsULong(index)
)
ByteOrder.BigEndian -> (
(getElementAsULong(index) shl 56)
or (getElementAsULong(index + 1) shl 48)
or (getElementAsULong(index + 2) shl 40)
or (getElementAsULong(index + 3) shl 32)
or (getElementAsULong(index + 4) shl 24)
or (getElementAsULong(index + 5) shl 16)
or (getElementAsULong(index + 6) shl 8)
or getElementAsULong(index + 7)
)
}
/**
* Helper for property accessors that care about endian encoding.
*
* @param bytes must be in MSB to LSB order. BigEndian will put array unchanged into buffer.
* LittleEndian will put bytes in LSB to MSB order in buffer.
*
*/
abstract fun putEndian(bytes: Array)
abstract fun shortToArray(short: Short): Array
abstract fun ushortToArray(ushort: UShort): Array
abstract fun intToArray(int: Int): Array
abstract fun uintToArray(int: UInt): Array
abstract fun longToArray(long: Long): Array
abstract fun ulongToArray(uLong: ULong): Array
companion object {
protected const val NO_MARK = -1
protected const val SHORT_LENGTH = 2
protected const val INT_LENGTH = 4
protected const val LONG_LENGTH = 8
fun checkBounds(
off: Int,
len: Int,
size: Int,
) { // package-private
if (off or len or off + len or size - (off + len) < 0) {
throw IndexOutOfBoundsException(
"off=$off, len=$len out of bounds (size=$size)",
)
}
}
}
}
@@ -0,0 +1,878 @@
/*
* 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.io
// Credits: skolson, from https://github.com/skolson/KmpIO
import kotlin.math.min
/**
* Port of a subset of Java's ByteBuffer with full Endian support. This class should be replaceable
* by an equivalent class in kotlinx-io once that library settles. This ByteBuffer is
* implemented on a ByteArray for simplicity
*/
abstract class ByteBufferBase<Element, Array> constructor(
capacity: Int,
order: ByteOrder = ByteOrder.LittleEndian,
override val isReadOnly: Boolean = false,
) : Buffer<Element, Array>(-1, 0, capacity, capacity, order),
Comparable<ByteBufferBase<Element, Array>> {
abstract var buf: Array
protected set
private var offset = 0
val contentBytes get() = buf
// The following properties offer simple encode/decode operations as indicated by the ByteOrder
// currently in effect. Properties are defined for many basic types
/**
* This property offers byte-level read/write. get returns the byte at the current position,
* and increments then position by one. set changes the byte at the current position, then
* increments the position by one.
*
* @throws IllegalStateException if the position is already at the limit
*/
override var byte: Element
get() {
checkPosition()
return getElementAt(position++)
}
set(value) {
checkPosition()
setElementAt(position++, value)
}
/**
* Compacts this buffer&nbsp;&nbsp;<i>(optional operation)</i>.
*
* <p> The bytes between the buffer's current position and its limit,
* if any, are copied to the beginning of the buffer. That is, the
* byte at index <i>p</i>&nbsp;=&nbsp;<tt>position()</tt> is copied
* to index zero, the byte at index <i>p</i>&nbsp;+&nbsp;1 is copied
* to index one, and so forth until the byte at index
* <tt>limit()</tt>&nbsp;-&nbsp;1 is copied to index
* <i>n</i>&nbsp;=&nbsp;<tt>limit()</tt>&nbsp;-&nbsp;<tt>1</tt>&nbsp;-&nbsp;<i>p</i>.
* The buffer's position is then set to <i>n+1</i> and its limit is set to
* its capacity. The mark, if defined, is discarded.
*
* <p> The buffer's position is set to the number of bytes copied,
* rather than to zero, so that an invocation of this method can be
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
*
* <p> Invoke this method after writing data from a buffer in case the
* write was incomplete. The following loop, for example, copies bytes
* from one channel to another via the buffer <tt>buf</tt>:
*
* <blockquote><pre>{@code
* buf.clear(); // Prepare buffer for use
* while (in.read(buf) >= 0 || buf.position != 0) {
* buf.flip();
* out.write(buf);
* buf.compact(); // In case of partial write
* }
* }</pre></blockquote>
*
* @return This buffer
*/
fun compact() {
var destIndex = 0
for (i in position until position + limit) {
setElementAt(destIndex++, getElementAt(i))
}
position = destIndex
limit = capacity
resetMark()
}
/**
* Compares this buffer to another.
*
* <p> Two byte buffers are compared by comparing their sequences of
* remaining elements lexicographically, without regard to the starting
* position of each sequence within its corresponding buffer.
*
* @return A negative integer, zero, or a positive integer as this buffer
* is less than, equal to, or greater than the given buffer
*/
override fun compareTo(other: ByteBufferBase<Element, Array>): Int {
val n: Int = position + min(remaining, other.remaining)
var i: Int = position
var j: Int = other.position
while (i < n) {
val r = compareElement(getElementAt(i), other.getElementAt(j))
if (r != 0) return r
i++
j++
}
return 0
}
abstract fun compareElement(
element: Element,
other: Element,
): Int
/**
* The contents of the current buffer are replaced with the contents of the source. Position,
* limit, capacity, order, and mark will all be set to same as the source.
*/
fun copy(source: ByteBufferBase<Element, Array>) {
buf = source.buf
position = source.position
limit = source.limit
mark = source.mark
order = source.order
}
/**
* Similar to [ByteArray] copyInto
* @param destination Array to receive copy
* @param destinationOffset index into destination where copy will start
* @param startIndex index in source array where copy will start
* @param endIndex index in source that copy will end, exclusive. [endIndex - startIndex] will
* be number of bytes copied.
*/
abstract fun copyInto(
destination: Array,
destinationOffset: Int = 0,
startIndex: Int = 0,
endIndex: Int,
)
/**
* Starting at the current position, copy bytes into the specified destination array. Increment
* position by the number of bytes retrieved.
*
* @param destination
* The array with sufficient capacity into which bytes will be written
* @param destinationOffset
* The offset into the destination at which the copy will start
* @param length
* The number of bytes to be copied
* @return This buffer
*/
fun fillArray(
destination: Array,
destinationOffset: Int = 0,
length: Int,
) {
checkBounds(destinationOffset, length, length)
if (length > remaining) {
throw IllegalStateException("Copying length:$length is more than remaining:$remaining")
}
copyInto(destination, destinationOffset, position, position + length)
position += length
}
/**
* <p> This method transfers the bytes remaining in the given source
* buffer into this buffer. If there are more bytes remaining in the
* source buffer than in this buffer, that is, if
* <tt>src.remaining()</tt>&nbsp;<tt>&gt;</tt>&nbsp;<tt>remaining()</tt>,
* then no bytes are transferred and an IllegalStateException is thrown
* </p>
*
* <p> Otherwise, this method copies
* <i>n</i>&nbsp;=&nbsp;<tt>src.remaining()</tt> bytes from the given
* buffer into this buffer, starting at each buffer's current position.
* The positions of both buffers are then incremented by <i>n</i>.
*/
open fun put(source: ByteBufferBase<Element, Array>) {
if (source == this) {
throw IllegalArgumentException("Cannot copy ByteBuffer to itself")
}
val sourceBytes = source.remaining
if (sourceBytes > remaining) {
throw IllegalArgumentException("Remaining source bytes:$sourceBytes exceeds destination remaining:$remaining")
}
source.fillArray(buf, position, sourceBytes)
position += sourceBytes
}
/**
* The following are all private functions
*/
private fun checkPosition(forLength: Int = 1) {
if (position + forLength > limit) {
throw IllegalStateException()
}
}
}
/**
* Enhance byte array implementing the Buffer interface which provides position/limit/remaining/capacity tracking, and
* support for either little endian or big endian encoding of basic numeric types. See [ByteBufferBase] and [Buffer] for
* more details.
* @param capacity starting size of buffer in bytes
* @param order defaults to little endian encoding of numeric types
* @param isReadOnly true if for some reason opeations that change content should throw an exception
* @param buf defaults to a ByteArray of specified [capacity]
*/
class ByteBuffer(
capacity: Int,
order: ByteOrder = ByteOrder.LittleEndian,
isReadOnly: Boolean = false,
override var buf: ByteArray = ByteArray(capacity),
) : ByteBufferBase<Byte, ByteArray>(capacity, order, isReadOnly) {
/**
* Construct a ByteBuffer from an existing ByteArray
* @param bytes becomes the buffer content (not a copy). capacity is set to the ByteArray size, position is set to
* zero
* @param order defaults to little endian encoding of numeric types
*/
constructor(bytes: ByteArray, order: ByteOrder = ByteOrder.LittleEndian) :
this(bytes.size, order, false, bytes)
override fun flip(): ByteBuffer {
super.flip()
return this
}
override fun getElementAt(index: Int): Byte = buf[index]
override fun setElementAt(
index: Int,
element: Byte,
) {
buf[index] = element
}
/**
* gets one byte at the current position without changing the position. The byte is treated as
* unsigned, so the returned Int will always be positive.
* @param index indicates which element in the current array to retrieve
* @return INt will not have it's high order bits set.
*/
override fun getElementAsInt(index: Int): Int = buf toPosInt index
/**
* gets one byte at the current position without changing the position, and return a UInt.
* The byte is treated as unsigned.
* @param index indicates which element in the current array to retrieve
* @return UINt will not have it's high order bits set.
*/
override fun getElementAsUInt(index: Int): UInt = buf toPosUInt index
/**
* gets one byte at the current position without changing the position. The byte is treated as
* unsigned, so the returned Long will always be positive.
* @param index indicates which element in the current array to retrieve
* @return Long will not have it's high order bits set.
*/
override fun getElementAsLong(index: Int): Long = buf toPosLong index
/**
* gets one byte at the current position without changing the position. The byte is treated as
* unsigned, so the returned ULong will always be positive.
* @param index indicates which element in the current array to retrieve
* @return ULong will not have it's high order bits set.
*/
override fun getElementAsULong(index: Int): ULong = buf toPosULong index
override fun getBytes(length: Int): ByteArray {
val l = min(remaining, length)
val a = ByteArray(l)
buf.copyInto(a, 0, position, position + l)
position += l
return a
}
override fun getBytes(bytes: ByteArray) {
val l = min(remaining, bytes.size)
buf.copyInto(bytes, 0, position, position + l)
position += l
}
override fun put(bytes: ByteArray) {
val l = min(remaining, bytes.size)
bytes.copyInto(buf, position, 0, l)
position += l
}
override fun putEndian(bytes: ByteArray) {
if (order == ByteOrder.LittleEndian) {
bytes.reverse()
}
put(bytes)
}
override fun shortToArray(short: Short): ByteArray =
byteArrayOf(
(short.toInt() shr 8).toByte(),
short.toByte(),
)
override fun ushortToArray(ushort: UShort): ByteArray =
byteArrayOf(
(ushort.toUInt() shr 8).toByte(),
ushort.toByte(),
)
override fun intToArray(int: Int): ByteArray =
byteArrayOf(
(int shr 24 and 0xff).toByte(),
(int shr 16 and 0xff).toByte(),
(int shr 8 and 0xff).toByte(),
(int and 0xff).toByte(),
)
override fun uintToArray(int: UInt): ByteArray =
byteArrayOf(
(int shr 24 and 0xffu).toByte(),
(int shr 16 and 0xffu).toByte(),
(int shr 8 and 0xffu).toByte(),
(int and 0xffu).toByte(),
)
override fun longToArray(long: Long): ByteArray =
byteArrayOf(
(long shr 56 and 0xff).toByte(),
(long shr 48 and 0xff).toByte(),
(long shr 40 and 0xff).toByte(),
(long shr 32 and 0xff).toByte(),
(long shr 24 and 0xff).toByte(),
(long shr 16 and 0xff).toByte(),
(long shr 8 and 0xff).toByte(),
(long and 0xff).toByte(),
)
override fun ulongToArray(uLong: ULong): ByteArray =
byteArrayOf(
(uLong shr 56 and 0xffu).toByte(),
(uLong shr 48 and 0xffu).toByte(),
(uLong shr 40 and 0xffu).toByte(),
(uLong shr 32 and 0xffu).toByte(),
(uLong shr 24 and 0xffu).toByte(),
(uLong shr 16 and 0xffu).toByte(),
(uLong shr 8 and 0xffu).toByte(),
(uLong and 0xffu).toByte(),
)
/**
* Similar to [ByteArray] copyInto
* @param destination Array to receive copy
* @param destinationOffset index into destination where copy will start
* @param startIndex index in source array where copy will start
* @param endIndex index in source that copy will end, exclusive. [endIndex - startIndex] will
* be number of bytes copied.
*/
override fun copyInto(
destination: ByteArray,
destinationOffset: Int,
startIndex: Int,
endIndex: Int,
) {
buf.copyInto(destination, destinationOffset, startIndex, endIndex)
}
override fun compareElement(
element: Byte,
other: Byte,
): Int = element.compareTo(other)
/**
* Increase size of buffer. Capacity and content are increased. Position is unchanged. if limit
* currently set to capacity, it will be set to the new capacity. All data is retained from
* previous buffer, including bytes between old limit and capacity.
* @param addCapacity bytes to add. Unsigned as can't be used to shrink.
*/
override fun expand(addCapacity: UInt) {
val changeLimit = limit == capacity
capacity += addCapacity.toInt()
val newBuf = ByteArray(capacity)
buf.copyInto(newBuf, 0, 0)
buf = newBuf
if (changeLimit) limit = capacity
}
/**
* Appends a buffer starting at its position, to the end of this buffer,
* starting at position of this buffer for appendBuffer remaining size. New position will
* be at new limit. If the contents must be expanded (usually is), then a new byteArray is allocated.
* position is moved to the new limit.
* @param appendBuffer buffer to be appended, starting at its poition for remaining bytes
* @return this
*/
fun expand(appendBuffer: ByteBuffer) {
val newLimit = position + appendBuffer.remaining
if (newLimit > capacity) {
val oldBuf = buf
buf = ByteArray(newLimit)
oldBuf.copyInto(buf, 0, 0, position)
}
appendBuffer.contentBytes.copyInto(
buf,
position,
appendBuffer.position,
appendBuffer.remaining,
)
capacity = newLimit
limit = newLimit
position = limit
}
fun get(
destination: ByteArray,
destinationOffset: Int = 0,
size: Int = destination.size,
) {
super.fillArray(destination, destinationOffset, size)
}
/**
* Copies specified bytes from source to this buffer, starting at position, for the
* specified length. Position is incremented by the length. Any bounds violation throws
* and IllegalArgumentException
*
* @param source byte array to write from, will be unchanged
* @param sourceOffset starting offset in source, defaults to 0
* @param length number of bytes to copy, defaults to size of source
* @return this
* @throws IllegalArgumentException on bounds violation
*/
fun putBytes(
source: ByteArray,
sourceOffset: Int = 0,
length: Int = source.size,
) {
checkBounds(sourceOffset, length, length)
if (length > remaining) {
throw IllegalArgumentException("Length:$length exceeds remaining:$remaining")
}
source.copyInto(buf, position, sourceOffset, length)
position += length
}
/**
* Make a new ByteBuffer containing the [remaining bytes] of this one. Length can be overridden to
* a shorter value than the default [remaining]. If length is > [remaining], [remaining] is used.
*
* Position in this ByteBuffer is unaffected. Position in new returned ByteBuffer is 0.
*
* @param length defaults to [remaining]. can be between 1 and [remaining]
*/
override fun slice(length: Int): ByteBuffer {
val l = min(remaining, length)
val bytes = ByteArray(l)
buf.copyInto(bytes, 0, position, position + l)
return ByteBuffer(bytes, this.order)
}
/**
* Convert from a [ByteBuffer] to a [UByteBuffer], retaining the same capacity, position, limit
* and contents
*/
fun toUByteBuffer(): UByteBuffer {
val uBuf = UByteBuffer(capacity, order, isReadOnly, contentBytes.toUByteArray())
uBuf.positionLimit(position, remaining)
return uBuf
}
override fun toString(): String =
buildString {
append("Position: $position, limit: $limit, remaining: $remaining. Content: 0x")
for (i in position until limit) {
append("${contentBytes[i].toString(16).padStart(2, '0')} ")
}
}
/**
* Tells whether or not this buffer is equal to another object.
*
* <p> Two byte buffers are equal if, and only if,
*
* <ol>
*
* <li><p> They have the same element type, </p></li>
*
* <li><p> They have the same number of remaining elements, and
* </p></li>
*
* <li><p> The two sequences of remaining elements, considered
* independently of their starting positions, are pointwise equal.
* </p></li>
*
* </ol>
*
* <p> A byte buffer is not equal to any other type of object. </p>
*
* @param other The object to which this buffer is to be compared
*
* @return <tt>true</tt> if, and only if, this buffer is equal to the
* given object
*/
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (this === other) return true
if (other !is ByteBuffer) return false
if (remaining != other.remaining) return false
var i = limit - 1
var j = other.limit - 1
while (i >= position) {
if (buf[i--] != other.buf[j--]) return false
}
return true
}
/**
* Returns the current hash code of this buffer.
*
* <p> The hash code of a byte buffer depends only upon its remaining
* elements; that is, upon the elements from <tt>position()</tt> up to, and
* including, the element at <tt>limit()</tt>&nbsp;-&nbsp;<tt>1</tt>.
*
* <p> Because buffer hash codes are content-dependent, it is inadvisable
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
* @return The current hash code of this buffer
*/
override fun hashCode(): Int {
var h = 1
val p: Int = position
for (i in limit - 1 downTo p) h = 31 * h + buf[i].toInt()
return h
}
}
class UByteBuffer(
capacity: Int,
order: ByteOrder = ByteOrder.LittleEndian,
isReadOnly: Boolean = false,
override var buf: UByteArray = UByteArray(capacity),
) : ByteBufferBase<UByte, UByteArray>(capacity, order, isReadOnly) {
constructor(bytes: UByteArray, order: ByteOrder = ByteOrder.LittleEndian) :
this(bytes.size, order, false, bytes)
/**
* Similar to [ByteArray] copyInto
* @param destination Array to receive copy
* @param destinationOffset index into destination where copy will start
* @param startIndex index in source array where copy will start
* @param endIndex index in source that copy will end, exclusive. [endIndex - startIndex] will
* be number of bytes copied.
*/
override fun copyInto(
destination: UByteArray,
destinationOffset: Int,
startIndex: Int,
endIndex: Int,
) {
buf.copyInto(destination, destinationOffset, startIndex, endIndex)
}
override fun compareElement(
element: UByte,
other: UByte,
): Int = element.compareTo(other)
/**
* Increase size of buffer. Capacity and content are increased. Position is unchanged. if limit
* currently set to capacity, it will be set to the new capacity. All data is retained from
* previous buffer, including bytes between old limit and capacity.
* @param addCapacity bytes to add. Unsigned as can't be used to shrink.
*/
override fun expand(addCapacity: UInt) {
val changeLimit = limit == capacity
capacity += addCapacity.toInt()
val newBuf = UByteArray(capacity)
buf.copyInto(newBuf, 0, 0)
buf = newBuf
if (changeLimit) limit = capacity
}
/**
* Appends a buffer starting at its position, to the end of this buffer,
* starting at position of this buffer for appendBuffer remaining size. New position will
* be at new limit. If the contents must be expanded (usually is), then a new byteArray is allocated.
* position is moved to the new limit.
* @param appendBuffer buffer to be appended, starting at its poition for remaining bytes
* @return this
*/
fun expand(appendBuffer: UByteBuffer) {
val newLimit = position + appendBuffer.remaining
if (newLimit > capacity) {
val oldBuf = buf
buf = UByteArray(newLimit)
oldBuf.copyInto(buf, 0, 0, position)
}
appendBuffer.contentBytes.copyInto(
buf,
position,
appendBuffer.position,
appendBuffer.remaining,
)
capacity = newLimit
limit = newLimit
position = limit
}
override fun flip(): UByteBuffer {
super.flip()
return this
}
override fun getElementAt(index: Int): UByte = buf[index]
/**
* gets one byte at the current position without changing the position. The byte is treated as
* unsigned, so the returned Int will always be positive.
* @param index indicates which element in the current array to retrieve
* @return Int will not have it's high order bits set.
*/
override fun getElementAsInt(index: Int): Int = buf toPosInt index
/**
* gets one byte at the current position without changing the position. The byte is treated as
* unsigned, so the returned UInt will always be positive.
* @param index indicates which element in the current array to retrieve
* @return UInt will not have it's high order bits set.
*/
override fun getElementAsUInt(index: Int): UInt = buf toPosUInt index
/**
* gets one byte at the current position without changing the position. The byte is treated as
* unsigned, so the returned Long will always be positive.
* @param index indicates which element in the current array to retrieve
* @return Long will not have it's high order bits set.
*/
override fun getElementAsLong(index: Int): Long = buf toPosLong index
/**
* gets one byte at the current position without changing the position. The byte is treated as
* unsigned, so the returned ULong will always be positive.
* @param index indicates which element in the current array to retrieve
* @return ULong will not have it's high order bits set.
*/
override fun getElementAsULong(index: Int): ULong = buf toPosULong index
fun get(
destination: UByteArray,
destinationOffset: Int = 0,
size: Int = destination.size,
) {
super.fillArray(destination, destinationOffset, size)
}
override fun getBytes(length: Int): UByteArray {
val l = min(remaining, length)
val a = UByteArray(l)
buf.copyInto(a, 0, position, position + l)
position += l
return a
}
override fun getBytes(bytes: UByteArray) {
val l = min(remaining, bytes.size)
buf.copyInto(bytes, 0, position, position + l)
position += l
}
override fun put(bytes: UByteArray) {
val l = min(remaining, bytes.size)
bytes.copyInto(buf, position, 0, l)
position += l
}
/**
* Copies specified bytes from source to this buffer, starting at position, for the
* specified length. Position is incremented by the length. Any bounds violation throws
* an IllegalArgumentException
*
* @param source byte array to write from, will be unchanged
* @param sourceOffset starting offset in source, defaults to 0
* @param length number of bytes to copy, defaults to size of source
* @return this
* @throws IllegalArgumentException on bounds violation
*/
fun putBytes(
source: UByteArray,
sourceOffset: Int = 0,
length: Int = source.size,
) {
checkBounds(sourceOffset, length, length)
if (length > remaining) {
throw IllegalArgumentException("Length:$length exceeds remaining:$remaining")
}
for (i in sourceOffset until sourceOffset + length) {
byte = source[i]
}
}
override fun putEndian(bytes: UByteArray) {
if (order == ByteOrder.LittleEndian) {
bytes.reverse()
}
put(bytes)
}
override fun setElementAt(
index: Int,
element: UByte,
) {
buf[index] = element
}
/**
* Make a new ByteBuffer containing the [remaining bytes] of this one. Length can be overriden to
* a shorter value than the default [remaining]. Position is unaffected
* @param length defaults to [remaining]. can be between 1 and [remaining]
*/
override fun slice(length: Int): UByteBuffer {
val bytes = UByteArray(length)
buf.copyInto(bytes, 0, position, position + length)
return UByteBuffer(bytes, this.order)
}
override fun shortToArray(short: Short): UByteArray =
ubyteArrayOf(
(short.toInt() shr 8).toUByte(),
short.toUByte(),
)
override fun ushortToArray(ushort: UShort): UByteArray =
ubyteArrayOf(
(ushort.toUInt() shr 8).toUByte(),
ushort.toUByte(),
)
override fun intToArray(int: Int): UByteArray =
ubyteArrayOf(
(int shr 24 and 0xff).toUByte(),
(int shr 16 and 0xff).toUByte(),
(int shr 8 and 0xff).toUByte(),
(int and 0xff).toUByte(),
)
override fun uintToArray(int: UInt): UByteArray =
ubyteArrayOf(
(int shr 24 and 0xffu).toUByte(),
(int shr 16 and 0xffu).toUByte(),
(int shr 8 and 0xffu).toUByte(),
(int and 0xffu).toUByte(),
)
override fun longToArray(long: Long): UByteArray =
ubyteArrayOf(
(long shr 56 and 0xff).toUByte(),
(long shr 48 and 0xff).toUByte(),
(long shr 40 and 0xff).toUByte(),
(long shr 32 and 0xff).toUByte(),
(long shr 24 and 0xff).toUByte(),
(long shr 16 and 0xff).toUByte(),
(long shr 8 and 0xff).toUByte(),
(long and 0xff).toUByte(),
)
override fun ulongToArray(uLong: ULong): UByteArray =
ubyteArrayOf(
(uLong shr 56 and 0xffu).toUByte(),
(uLong shr 48 and 0xffu).toUByte(),
(uLong shr 40 and 0xffu).toUByte(),
(uLong shr 32 and 0xffu).toUByte(),
(uLong shr 24 and 0xffu).toUByte(),
(uLong shr 16 and 0xffu).toUByte(),
(uLong shr 8 and 0xffu).toUByte(),
(uLong and 0xffu).toUByte(),
)
/**
* Convert from a [UByteBuffer] to a [ByteBuffer], retaining the same capacity, position, limit
* and contents
*/
fun toByteBuffer(): ByteBuffer {
val uBuf = ByteBuffer(capacity, order, isReadOnly, contentBytes.toByteArray())
uBuf.positionLimit(position, remaining)
return uBuf
}
override fun toString(): String =
buildString {
append("Position: $position, limit: $limit, remaining: $remaining. Content: 0x")
for (i in position until limit) {
append("${contentBytes[i].toString(16).padStart(2, '0')} ")
}
}
/**
* Tells whether or not this buffer is equal to another object.
*
* <p> Two byte buffers are equal if, and only if,
*
* <ol>
*
* <li><p> They have the same element type, </p></li>
*
* <li><p> They have the same number of remaining elements, and
* </p></li>
*
* <li><p> The two sequences of remaining elements, considered
* independently of their starting positions, are pointwise equal.
* </p></li>
*
* </ol>
*
* <p> A byte buffer is not equal to any other type of object. </p>
*
* @param other The object to which this buffer is to be compared
*
* @return <tt>true</tt> if, and only if, this buffer is equal to the
* given object
*/
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (this === other) return true
if (other !is UByteBuffer) return false
if (remaining != other.remaining) return false
var i = limit - 1
var j = other.limit - 1
while (i >= position) {
if (buf[i--] != other.buf[j--]) return false
}
return true
}
/**
* Returns the current hash code of this buffer.
*
* <p> The hash code of a byte buffer depends only upon its remaining
* elements; that is, upon the elements from <tt>position()</tt> up to, and
* including, the element at <tt>limit()</tt>&nbsp;-&nbsp;<tt>1</tt>.
*
* <p> Because buffer hash codes are content-dependent, it is inadvisable
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
* @return The current hash code of this buffer
*/
override fun hashCode(): Int {
var h = 1
val p: Int = position
for (i in limit - 1 downTo p) h = 31 * h + buf[i].toInt()
return h
}
}
@@ -0,0 +1,392 @@
/*
* 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.io
// Credits: skolson, from https://github.com/skolson/KmpIO
import kotlin.math.max
class CustomBitSet(
val numberOfBits: Int,
) {
private var words =
LongArray(wordIndex(numberOfBits - 1) + 1) { 0L }
private val wordsInUse: Int
get() {
var i = words.size - 1
while (i >= 0) {
if (words[i] != 0L) break
i--
}
return i + 1
}
val length: Int
get() {
if (wordsInUse == 0) {
return 0
}
return BITS_PER_WORD * (wordsInUse - 1) +
(BITS_PER_WORD - numberOfLeadingZeros(words[wordsInUse - 1]))
}
constructor(bytes: ByteArray, bitsCount: Int = bytes.size * 8) : this(bitsCount) {
transformBuffer(ByteBuffer(bytes))
}
// constructor(buffer: ByteBuffer, bitsCount: Int = buffer.remaining * 8) : this(bitsCount) {
// transformBuffer(buffer)
// }
//
private fun transformBuffer(buffer: ByteBuffer) {
words = LongArray((buffer.remaining + 7) / 8) { 0 }
var i = 0
while (buffer.remaining >= 8) words[i++] = buffer.long
var j = 0
while (buffer.remaining > 0) {
words[i] = words[i] or ((buffer.byte.toLong() and 0xffL) shl (8 * j))
j++
}
}
val empty: Boolean
get() {
return wordsInUse == 0
}
fun toByteArray(): ByteArray {
val n = wordsInUse
if (n == 0) return ByteArray(0)
var len = 8 * (n - 1)
var x = words[n - 1]
while (x != 0L) {
len++
x = x ushr 8
}
val sz = if (numberOfBits % 8 > 0) (numberOfBits / 8) + 1 else numberOfBits / 8
val bytes = ByteArray(sz)
val bb = ByteBuffer(bytes)
bb.order = Buffer.ByteOrder.LittleEndian
for (i in 0 until n - 1) bb.long = words[i]
x = words[n - 1]
while (x != 0L) {
bb.byte = (x and 0xff).toByte()
x = x ushr 8
}
return bytes
}
fun flip(bitIndex: Int) {
if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
val wordIndex = wordIndex(bitIndex)
expandTo(wordIndex)
words[wordIndex] = words[wordIndex] xor (1L shl bitIndex)
checkInvariants()
}
fun flip(
fromIndex: Int,
toIndex: Int,
) {
checkRange(fromIndex, toIndex)
if (fromIndex == toIndex) return
val startWordIndex: Int = wordIndex(fromIndex)
val endWordIndex: Int = wordIndex(toIndex - 1)
expandTo(endWordIndex)
val firstWordMask: Long = WORD_MASK shl fromIndex
val lastWordMask: Long = WORD_MASK ushr -toIndex
if (startWordIndex == endWordIndex) {
// Case 1: One word
words[startWordIndex] =
words[startWordIndex] xor (firstWordMask and lastWordMask)
} else {
// Case 2: Multiple words
// Handle first word
words[startWordIndex] = words[startWordIndex] xor firstWordMask
// Handle intermediate words, if any
for (i in startWordIndex + 1 until endWordIndex) {
words[i] =
words[i] xor WORD_MASK
}
// Handle last word
words[endWordIndex] = words[endWordIndex] xor lastWordMask
}
checkInvariants()
}
operator fun get(bitIndex: Int): Boolean {
if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
checkInvariants()
val wordIndex = wordIndex(bitIndex)
return (
wordIndex < wordsInUse &&
words[wordIndex] and (1L shl bitIndex) != 0L
)
}
operator fun set(
bitIndex: Int,
on: Boolean,
) {
if (!on) {
clear(bitIndex)
return
}
if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
val wordIndex: Int = wordIndex(bitIndex)
expandTo(wordIndex)
words[wordIndex] = words[wordIndex] or (1L shl bitIndex) // Restores invariants
checkInvariants()
}
fun iterateSetBits(
startIndex: Int = 0,
onSetBit: (Int) -> Boolean,
): Int {
var index = nextSetBit(startIndex)
var count = 0
while (index >= 0) {
count++
if (!onSetBit(index)) break
index = nextSetBit(index + 1)
}
return count
}
fun nextSetBit(fromIndex: Int): Int {
if (fromIndex < 0) throw IndexOutOfBoundsException("fromIndex < 0: $fromIndex")
checkInvariants()
var u = wordIndex(fromIndex)
if (u >= wordsInUse) return -1
var word = words[u] and (WORD_MASK shl fromIndex)
while (true) {
if (word != 0L) return u * BITS_PER_WORD + numberOfTrailingZeros(word)
if (++u == wordsInUse) return -1
word = words[u]
}
}
fun iterateClearBits(
startIndex: Int = 0,
onClearedBit: (Int) -> Boolean,
): Int {
var index = nextClearBit(startIndex)
var count = 0
while (index < numberOfBits) {
count++
if (!onClearedBit(index)) break
index = nextClearBit(index + 1)
}
return count
}
fun nextClearBit(fromIndex: Int): Int {
if (fromIndex < 0) throw IndexOutOfBoundsException("fromIndex < 0: $fromIndex")
checkInvariants()
var u = wordIndex(fromIndex)
if (u >= wordsInUse) return fromIndex
var word = words[u].inv() and (WORD_MASK shl fromIndex)
while (true) {
if (word != 0L) return u * BITS_PER_WORD + numberOfTrailingZeros(word)
if (++u == wordsInUse) return wordsInUse * BITS_PER_WORD
word = words[u].inv()
}
}
fun clear(bitIndex: Int) {
if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
val wordIndex = wordIndex(bitIndex)
if (wordIndex >= wordsInUse) return
words[wordIndex] = words[wordIndex] and (1L shl bitIndex).inv()
checkInvariants()
}
fun clear() {
for (i in words.indices) words[i] = 0
}
private constructor(longArray: LongArray) : this(0) {
words = longArray
}
private fun ensureCapacity(wordsRequired: Int) {
if (words.size < wordsRequired) {
// Allocate larger of doubled size or required size
val request: Int = max(2 * words.size, wordsRequired)
words = words.copyOf(request)
}
}
private fun expandTo(wordIndex: Int) {
val wordsRequired = wordIndex + 1
if (wordsInUse < wordsRequired) {
ensureCapacity(wordsRequired)
}
}
private fun checkInvariants() {
if (wordsInUse == 0 || words[wordsInUse - 1] != 0L) {
if (wordsInUse >= 0 && wordsInUse <= words.size) {
if (wordsInUse == words.size || words[wordsInUse] == 0L) {
return
}
}
}
throw IllegalStateException("CustomBitSet check failed. wordsInUse:$wordsInUse, words:${words.size}")
}
override fun toString(): String {
var text = ""
var count = 0
iterateSetBits {
text = "$text, $it"
count++ < 50
}
return text
}
fun size(): Int = words.size * BITS_PER_WORD
companion object {
private const val ADDRESS_BITS_PER_WORD = 6
private const val BITS_PER_WORD = 1 shl ADDRESS_BITS_PER_WORD
private const val BIT_INDEX_MASK = BITS_PER_WORD - 1
private const val WORD_MASK = -0x1L
private fun wordIndex(bitIndex: Int): Int = bitIndex shr ADDRESS_BITS_PER_WORD
fun numberOfLeadingZeros(i: Long): Int {
// HD, Figure 5-6
if (i == 0L) return 64
var n = 1
var x = (i ushr 32).toInt()
if (x == 0) {
n += 32
x = i.toInt()
}
if (x ushr 16 == 0) {
n += 16
x = x shl 16
}
if (x ushr 24 == 0) {
n += 8
x = x shl 8
}
if (x ushr 28 == 0) {
n += 4
x = x shl 4
}
if (x ushr 30 == 0) {
n += 2
x = x shl 2
}
n -= x ushr 31
return n
}
fun numberOfTrailingZeros(i: Long): Int {
// HD, Figure 5-14
var x: Int
var y: Int
if (i == 0L) return 64
var n = 63
y = i.toInt()
if (y != 0) {
n -= 32
x = y
} else {
x = (i ushr 32).toInt()
}
y = x shl 16
if (y != 0) {
n -= 16
x = y
}
y = x shl 8
if (y != 0) {
n -= 8
x = y
}
y = x shl 4
if (y != 0) {
n -= 4
x = y
}
y = x shl 2
if (y != 0) {
n -= 2
x = y
}
return n - (x shl 1 ushr 31)
}
fun create(longs: LongArray): CustomBitSet {
var n: Int
n = longs.size
while (n > 0 && longs[n - 1] == 0L) {
n--
}
return CustomBitSet(longs.copyOf(n))
}
private fun checkRange(
fromIndex: Int,
toIndex: Int,
) {
if (fromIndex < 0) throw IndexOutOfBoundsException("fromIndex < 0: $fromIndex")
if (toIndex < 0) throw IndexOutOfBoundsException("toIndex < 0: $toIndex")
if (fromIndex > toIndex) {
throw IndexOutOfBoundsException(
"fromIndex: " + fromIndex +
" > toIndex: " + toIndex,
)
}
}
fun create(bbIn: ByteBuffer): CustomBitSet {
val bb = bbIn.slice()
bb.order = Buffer.ByteOrder.LittleEndian
var n = bb.remaining
while (n > 0 && bb.getElementAsInt(n - 1) == 0) {
n--
}
val words = LongArray((n + 7) / 8)
bb.limit = n
var i = 0
while (bb.remaining >= 8) words[i++] = bb.long
val remaining: Int = bb.remaining
var j = 0
while (j < remaining) {
words[i] = words[i] or ((bb.byte.toLong() and 0xffL) shl (8 * j))
j++
}
return CustomBitSet(words)
}
}
}
@@ -0,0 +1,489 @@
/*
* 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.io
// Credits: skolson, from https://github.com/skolson/KmpIO
/**
* Simple extension to translate a ByteArray to a hex string
* @param startIndex index in array to start, defaults to zero
* @param length number of bytes to turn to hex
* @return String of size [2 * length], all lower case
* @throws IndexOutOfBoundsException if argument(s) specified are wrong
*/
fun ByteArray.toHex(
startIndex: Int = 0,
length: Int = size,
): String {
val hexChars = "0123456789abcdef"
val result = StringBuilder(length * 2)
for (i in startIndex until startIndex + length) {
result.append(hexChars[(this[i].toInt() and 0xF0).ushr(4)])
result.append(hexChars[this[i].toInt() and 0x0F])
}
return result.toString()
}
/**
* Convenience method, forces a Byte to an Int while stripping all sign bits. the Byte becomes
* the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
*/
infix fun ByteArray.toPosInt(index: Int): Int = this[index].toInt() and 0xFF
/**
* Convenience method, forces a Byte to an UInt while stripping all sign bits. the Byte becomes
* the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
*/
infix fun ByteArray.toPosUInt(index: Int): UInt = this[index].toUInt() and 0xFFu
/**
* Convenience method, forces a UByte to an Int while stripping all sign bits. the Byte becomes
* the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
*/
infix fun UByteArray.toPosInt(index: Int): Int = this[index].toInt() and 0xFF
/**
* Convenience method, forces a Byte to an UInt while stripping all sign bits. the Byte becomes
* the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
*/
infix fun UByteArray.toPosUInt(index: Int): UInt = this[index].toUInt() and 0xFFu
/**
* Convenience method, forces a Byte to a Long while stripping all sign bits. the Byte becomes
* the LSB of the Long produced. For example, byte of 0xFF becomes 0x00000000000000FF or 255 as
* the result Long.
*/
infix fun ByteArray.toPosLong(index: Int): Long = this[index].toLong() and 0xFF
/**
* Convenience method, forces a Byte to a ULong while stripping all sign bits. the Byte becomes
* the LSB of the Long produced. For example, byte of 0xFF becomes 0x00000000000000FF or 255 as
* the result Long.
*/
infix fun ByteArray.toPosULong(index: Int): ULong = this[index].toULong() and 0xFFu
/**
* Convenience method, forces a UByte to a Long while stripping all sign bits. the Byte becomes
* the LSB of the Long produced. For example, byte of 0xFF becomes 0x00000000000000FF or 255 as
* the result Int.
*/
infix fun UByteArray.toPosLong(index: Int): Long = this[index].toLong() and 0xFF
/**
* Convenience method, forces a UByte to a ULong while using only the LSB.
* For example, byte of 0xFF becomes 0x00000000000000FF or 255 as the result ULong.
*/
infix fun UByteArray.toPosULong(index: Int): ULong = this[index].toULong() and 0xFFu
// These endian-aware extensions functions assist with retrieving Short, UShort, Int, UInt, Long, Ulong, Float,
// and Double values from ByteArray and UByteArray. For some reason Kotlin only offers these for Little
// Endian (you have to do your own reverse() for BigEndian) and only in Kotlin Native.
/**
* Change one byte into an Int with toPosInt, then Binary Shift left the specified number of times.
* @param index of byte to change to an Int.
* @param shift number of times value is binary shifted left.
* @return resulting Int
*/
fun ByteArray.toIntShl(
index: Int,
shift: Int = 0,
): Int = this toPosInt index shl shift
/**
* Change one byte into an Int with toPosInt, then Binary Shift left the specified number of times.
* @param index of byte to change to an Int.
* @param shift number of times value is binary shifted left.
* @return result as UInt
*/
fun ByteArray.toUIntShl(
index: Int,
shift: Int = 0,
): UInt = this toPosUInt index shl shift
/**
* Change one byte into a Long, after the byte value is Binary Shifted left the specified number of times.
* @param index of byte to change to an Long.
* @param shift number of times value is binary shifted left.
* @return resulting Long
*/
fun ByteArray.toLongShl(
index: Int,
shift: Int = 0,
): Long = this toPosLong index shl shift
/**
* Change one byte into a ULong, after the byte value is Binary Shifted left the specified number of times.
* @param index of byte to change to an Long.
* @param shift number of times value is binary shifted left.
* @return resulting ULong
*/
fun ByteArray.toULongShl(
index: Int,
shift: Int = 0,
): ULong = this toPosULong index shl shift
/**
* starting at the specified index, change bytes at index and index+1 to a Short. Both LittleEndian
* and BigEndian encoding schemes are supported.
* @param index where two bytes to be converted to short start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting Short
*/
fun ByteArray.getShortAt(
index: Int,
littleEndian: Boolean = true,
): Short =
if (littleEndian) {
(toIntShl(index + 1, 8) or toIntShl(index)).toShort()
} else {
(toIntShl(index, 8) or toIntShl(index + 1)).toShort()
}
/**
* starting at the specified index, change bytes at index and index+1 to a UShort. Both LittleEndian
* and BigEndian encoding schemes are supported.
* @param index where two bytes to be converted to UShort start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting UShort
*/
fun ByteArray.getUShortAt(
index: Int,
littleEndian: Boolean = true,
): UShort =
if (littleEndian) {
(toUIntShl(index + 1, 8) or toUIntShl(index)).toUShort()
} else {
(toUIntShl(index, 8) or toUIntShl(index + 1)).toUShort()
}
/**
* starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an Int.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where four bytes to be converted to Int start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting Int
*/
fun ByteArray.getIntAt(
index: Int,
littleEndian: Boolean = true,
): Int =
if (littleEndian) {
toIntShl(index + 3, 24) or
toIntShl(index + 2, 16) or
toIntShl(index + 1, 8) or
toIntShl(index)
} else {
toIntShl(index, 24) or
toIntShl(index + 1, 16) or
toIntShl(index + 2, 8) or
toIntShl(index + 3)
}
/**
* starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an UInt.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where four bytes to be converted to UInt start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting UInt
*/
fun ByteArray.getUIntAt(
index: Int,
littleEndian: Boolean = true,
): UInt =
if (littleEndian) {
toUIntShl(index + 3, 24) or
toUIntShl(index + 2, 16) or
toUIntShl(index + 1, 8) or
toUIntShl(index)
} else {
toUIntShl(index, 24) or
toUIntShl(index + 1, 16) or
toUIntShl(index + 2, 8) or
toUIntShl(index + 3)
}
/**
* Starting at the specified index, change bytes at [index..index+7] to a Long.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where eight bytes to be converted to Long start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting Long
*/
fun ByteArray.getLongAt(
index: Int,
littleEndian: Boolean = true,
): Long =
if (littleEndian) {
toLongShl(index + 7, 56) or
toLongShl(index + 6, 48) or
toLongShl(index + 5, 40) or
toLongShl(index + 4, 32) or
toLongShl(index + 3, 24) or
toLongShl(index + 2, 16) or
toLongShl(index + 1, 8) or
toLongShl(index)
} else {
toLongShl(index, 56) or
toLongShl(index + 1, 48) or
toLongShl(index + 2, 40) or
toLongShl(index + 3, 32) or
toLongShl(index + 4, 24) or
toLongShl(index + 5, 16) or
toLongShl(index + 6, 8) or
toLongShl(index + 7)
}
/**
* Starting at the specified index, change bytes at [index..index+7] to a ULong.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where eight bytes to be converted to ULong start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting ULong
*/
fun ByteArray.getULongAt(
index: Int,
littleEndian: Boolean = true,
): ULong =
if (littleEndian) {
toULongShl(index + 7, 56) or
toULongShl(index + 6, 48) or
toULongShl(index + 5, 40) or
toULongShl(index + 4, 32) or
toULongShl(index + 3, 24) or
toULongShl(index + 2, 16) or
toULongShl(index + 1, 8) or
toULongShl(index)
} else {
toULongShl(index, 56) or
toULongShl(index + 1, 48) or
toULongShl(index + 2, 40) or
toULongShl(index + 3, 32) or
toULongShl(index + 4, 24) or
toULongShl(index + 5, 16) or
toULongShl(index + 6, 8) or
toULongShl(index + 7)
}
/**
* Simple extension to translate a ByteArray to a hex string
* @param startIndex index in array to start, defaults to zero
* @param length number of bytes to turn to hex
* @return String of size [2 * length], all lower case
* @throws IndexOutOfBoundsException if argument(s) specified are wrong
*/
fun UByteArray.toHex(
startIndex: Int = 0,
length: Int = size,
): String {
val hexChars = "0123456789abcdef"
val result = StringBuilder(length * 2)
for (i in startIndex until startIndex + length) {
result.append(hexChars[(this[i].toInt() and 0xF0).ushr(4)])
result.append(hexChars[this[i].toInt() and 0x0F])
}
return result.toString()
}
/**
* Change one byte into an Int, after the byte value is Binary Shifted left the specified number of times.
* @param index of byte to change to an Int.
* @param shift number of times value is binary shifted left.
* @return resulting Int
*/
fun UByteArray.toIntShl(
index: Int,
shift: Int = 0,
): Int = this toPosInt index shl shift
/**
* Change one byte into an UInt, after the byte value is Binary Shifted left the specified number of times.
* @param index of byte to change to an Int.
* @param shift number of times value is binary shifted left.
* @return resulting UInt
*/
fun UByteArray.toUIntShl(
index: Int,
shift: Int = 0,
): UInt = this toPosUInt index shl shift
/**
* Change one byte into a Long, after the byte value is Binary Shifted left the specified number of times.
* @param index of byte to change to an Long.
* @param shift number of times value is binary shifted left.
* @return resulting Long
*/
fun UByteArray.toLongShl(
index: Int,
shift: Int = 0,
): Long = this toPosLong index shl shift
/**
* Change one byte into a ULong, after the byte value is Binary Shifted left the specified number of times.
* @param index of byte to change to an Long.
* @param shift number of times value is binary shifted left.
* @return resulting ULong
*/
fun UByteArray.toULongShl(
index: Int,
shift: Int = 0,
): ULong = this toPosULong index shl shift
/**
* starting at the specified index, change bytes at index and index+1 to a Short. Both LittleEndian
* and BigEndian encoding schemes are supported.
* @param index where two bytes to be converted to short start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting Short
*/
fun UByteArray.getShortAt(
index: Int,
littleEndian: Boolean = true,
): Short =
if (littleEndian) {
(toIntShl(index + 1, 8) or toIntShl(index)).toShort()
} else {
(toIntShl(index, 8) or toIntShl(index + 1)).toShort()
}
/**
* starting at the specified index, change bytes at index and index+1 to a UShort. Both LittleEndian
* and BigEndian encoding schemes are supported.
* @param index where two bytes to be converted to UShort start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting UShort
*/
fun UByteArray.getUShortAt(
index: Int,
littleEndian: Boolean = true,
): UShort =
if (littleEndian) {
(toUIntShl(index + 1, 8) or toUIntShl(index)).toUShort()
} else {
(toUIntShl(index, 8) or toUIntShl(index + 1)).toUShort()
}
/**
* starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an Int.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where four bytes to be converted to Int start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting Int
*/
fun UByteArray.getIntAt(
index: Int,
littleEndian: Boolean = true,
): Int =
if (littleEndian) {
toIntShl(index + 3, 24) or
toIntShl(index + 2, 16) or
toIntShl(index + 1, 8) or
toIntShl(index)
} else {
toIntShl(index, 24) or
toIntShl(index + 1, 16) or
toIntShl(index + 2, 8) or
toIntShl(index + 3)
}
/**
* starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an UInt.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where four bytes to be converted to UInt start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting UInt
*/
fun UByteArray.getUIntAt(
index: Int,
littleEndian: Boolean = true,
): UInt =
if (littleEndian) {
toUIntShl(index + 3, 24) or
toUIntShl(index + 2, 16) or
toUIntShl(index + 1, 8) or
toUIntShl(index)
} else {
toUIntShl(index, 24) or
toUIntShl(index + 1, 16) or
toUIntShl(index + 2, 8) or
toUIntShl(index + 3)
}
/**
* Starting at the specified index, change bytes at [index..index+7] to a Long.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where eight bytes to be converted to Long start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting Long
*/
fun UByteArray.getLongAt(
index: Int,
littleEndian: Boolean = true,
): Long =
if (littleEndian) {
toLongShl(index + 7, 56) or
toLongShl(index + 6, 48) or
toLongShl(index + 5, 40) or
toLongShl(index + 4, 32) or
toLongShl(index + 3, 24) or
toLongShl(index + 2, 16) or
toLongShl(index + 1, 8) or
toLongShl(index)
} else {
toLongShl(index, 56) or
toLongShl(index + 1, 48) or
toLongShl(index + 2, 40) or
toLongShl(index + 3, 32) or
toLongShl(index + 4, 24) or
toLongShl(index + 5, 16) or
toLongShl(index + 6, 8) or
toLongShl(index + 7)
}
/**
* Starting at the specified index, change bytes at [index..index+7] to a ULong.
* Both LittleEndian and BigEndian encoding schemes are supported.
* @param index where eight bytes to be converted to ULong start
* @param littleEndian defaults to true for LittleEndian, or false for BigEndian
* @return the resulting ULong
*/
fun UByteArray.getULongAt(
index: Int,
littleEndian: Boolean = true,
): ULong =
if (littleEndian) {
toULongShl(index + 7, 56) or
toULongShl(index + 6, 48) or
toULongShl(index + 5, 40) or
toULongShl(index + 4, 32) or
toULongShl(index + 3, 24) or
toULongShl(index + 2, 16) or
toULongShl(index + 1, 8) or
toULongShl(index)
} else {
toULongShl(index, 56) or
toULongShl(index + 1, 48) or
toULongShl(index + 2, 40) or
toULongShl(index + 3, 32) or
toULongShl(index + 4, 24) or
toULongShl(index + 5, 16) or
toULongShl(index + 6, 8) or
toULongShl(index + 7)
}
@@ -20,37 +20,62 @@
*/
package com.vitorpamplona.quartz.utils.mac
import dev.whyoleg.cryptography.CryptographyProvider
import dev.whyoleg.cryptography.algorithms.HMAC
import dev.whyoleg.cryptography.algorithms.SHA256
import dev.whyoleg.cryptography.algorithms.SHA512
import dev.whyoleg.cryptography.providers.apple.Apple
actual class MacInstance actual constructor(
algorithm: String,
key: ByteArray,
) {
private val cryptoProvider = CryptographyProvider.Apple
private var internalMacInstance: HMAC.Key =
cryptoProvider
.get(HMAC)
.keyDecoder(digestForAlgorithm(algorithm))
.decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key)
private var hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction()
actual fun init(
key: ByteArray,
algorithm: String,
) {
TODO("Not yet implemented")
internalMacInstance =
cryptoProvider
.get(HMAC)
.keyDecoder(digestForAlgorithm(algorithm))
.decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key)
hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction()
}
actual fun getMacLength(): Int {
TODO("Not yet implemented")
}
actual fun getMacLength(): Int = hmacSignFunction.signIntoByteArray(internalMacInstance.encodeToByteArrayBlocking(HMAC.Key.Format.RAW))
actual fun update(array: ByteArray) {
TODO("Not yet implemented")
hmacSignFunction.update(array)
}
actual fun update(byte: Byte) {
TODO("Not yet implemented")
hmacSignFunction.update(byteArrayOf(byte))
}
actual fun doFinal(): ByteArray {
TODO("Not yet implemented")
}
actual fun doFinal(): ByteArray = hmacSignFunction.signToByteArray()
actual fun doFinal(
output: ByteArray,
offset: Int,
) {
TODO("Not yet implemented")
hmacSignFunction.signIntoByteArray(output, offset)
}
private fun digestForAlgorithm(algorithm: String) =
when (algorithm) {
"HmacSHA256" -> SHA256
"HmacSHA512" -> SHA512
else -> error("Algorithm is not yet supported.")
}
}
@@ -20,37 +20,30 @@
*/
package com.vitorpamplona.quartz
import kotlinx.cinterop.BetaInteropApi
import dev.whyoleg.cryptography.CryptographyProviderApi
import dev.whyoleg.cryptography.providers.base.toByteArray
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSBundle
import kotlinx.cinterop.toKString
import kotlinx.io.files.FileNotFoundException
import platform.Foundation.NSData
import platform.Foundation.NSString
import platform.Foundation.NSUTF8StringEncoding
import platform.Foundation.dataWithContentsOfFile
import platform.Foundation.stringWithContentsOfFile
import platform.darwin.NSObject
import platform.darwin.NSObjectMeta
import platform.posix.getenv
actual class TestResourceLoader {
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
@OptIn(ExperimentalForeignApi::class)
actual fun loadString(file: String): String {
// Split the filename and extension (e.g., "data.json" -> "data", "json")
val basename = file.substringBeforeLast(".")
val extension = file.substringAfterLast(".", "")
// Locate the file in the main application bundle
val path =
NSBundle.mainBundle.pathForResource(basename, ofType = extension)
?: throw IllegalArgumentException("Resource not found: $file")
// Read the file content as a UTF-8 string
return NSString
.stringWithContentsOfFile(
path = path,
encoding = NSUTF8StringEncoding,
error = null,
).toString()
val resourceDir = getenv("TEST_RESOURCES_ROOT")?.toKString()
val filePath = "$resourceDir/$file"
return NSString.stringWithContentsOfFile(filePath, encoding = NSUTF8StringEncoding, error = null) as String
}
private class BundleMarker : NSObject() {
companion object : NSObjectMeta()
@OptIn(ExperimentalForeignApi::class, CryptographyProviderApi::class)
fun loadFileData(file: String): ByteArray {
val resourceDir = getenv("TEST_RESOURCES_ROOT")?.toKString()
val filePath = "$resourceDir/$file"
return NSData.dataWithContentsOfFile(filePath)?.toByteArray() ?: throw FileNotFoundException("Resource $file was not found.")
}
}
@@ -0,0 +1,113 @@
/*
* 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.nip04Dm
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class EncryptionTest {
private val nip04 = Encryption()
val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray()
val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray()
val pk1 = Nip01.pubKeyCreate(sk1)
val pk2 = Nip01.pubKeyCreate(sk2)
val expectedShared = "7ce22696eb0e303ddaa491bdf2a56b79d249f2d861b8e012a933e01dc4beba81"
@Test
fun conversationKeyTest() {
assertEquals(
expectedShared,
nip04.computeSharedSecret(sk2, pk1).toHexKey(),
)
assertEquals(
expectedShared,
nip04.computeSharedSecret(sk1, pk2).toHexKey(),
)
}
@Test
fun encryptDecryptTest() {
val message = "testing"
val cipher = nip04.encrypt(message, sk2, pk1)
assertEquals(message, nip04.decrypt(cipher, sk2, pk1))
assertEquals(message, nip04.decrypt(cipher, sk1, pk2))
val cipher2 = nip04.encrypt(message, sk1, pk2)
assertEquals(message, nip04.decrypt(cipher2, sk2, pk1))
assertEquals(message, nip04.decrypt(cipher2, sk1, pk2))
}
@Test
fun decryptTest() {
val cipher = "zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="
assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk2, pk1)))
assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk1, pk2)))
}
@Test
fun decryptLargePayloadTest() {
val ciphertext =
"6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="
val expected = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk2, pk1)))
assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk1, pk2)))
}
@Test
fun isNIP04Encode() {
assertTrue(EncryptedInfo.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA=="))
assertTrue(EncryptedInfo.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
assertTrue(
EncryptedInfo.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="),
)
}
@Test
fun isNIP04EncodeWithBug() {
assertTrue(
EncryptedInfo.isNIP04(
"QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" +
"TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" +
"xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" +
"ASafA/T5z4xs8oG88pSe9wSbSsw0xNjJeh3xLRCLuEuA9KI8hQ1Ys9nEax2UlaB/IL3o77" +
"OwBL+rrdUbNHTxYifgygRhg3BaXMsXRFNJbqYeMaRaNbvHkLVAQV2jLY4P/cKHBjEcTC/f" +
"lrCc2NCYF34rOQUY5EJVnFzM8qYVw6xNupBHTS7WFx1r60cPjG19P/+yoiTZ6bPdHTU0X2" +
"t64ovF2YWUq6/iKAclMaZDhWfrKqf82e62oIff55WQw2bw8A/jtBQVCf66EtEJ2OSFxNaZ" +
"rO+A4oLkHDCnAV+6fYzwo89gPOvORcVvSvg55yGiBFUZx9EHS6kdH1SU80/Mbxe2oI=" +
"?iv=gxz9pUFJFZHuV+D+hgKEOw==-null",
),
)
}
}
@@ -0,0 +1,54 @@
/*
* 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.nip04Dm
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import kotlin.test.Test
import kotlin.test.assertEquals
class Nip04Test {
@Test
fun encryptDecryptNIP4Test() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptSharedSecretDecryptNIP4Test() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
}
@@ -0,0 +1,72 @@
/*
* 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.nip17Dm
import com.vitorpamplona.quartz.TestResourceLoader
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
import kotlin.test.Test
import kotlin.test.assertEquals
class AESGCMTest {
val decryptionNonce = "01e77c94bd5aba3e3cbb69594e7ba07c"
val decryptionKey = "c128ecffab90ee7810e3df08e7fb2cc39a8d40f24201f48b2b36e23b34ac50ee"
val cipher =
AESGCM(
decryptionKey.hexToByteArray(),
decryptionNonce.encodeToByteArray(),
)
@Test
fun encryptDecrypt() {
val encrypted = cipher.encrypt("Testing".encodeToByteArray())
val decrypted = cipher.decrypt(encrypted)
assertEquals("Testing", decrypted.decodeToString())
}
@Test
fun imageTest() {
val image =
TestResourceLoader().loadFileData("ovxxk2vz.jpg")
val decrypted = cipher.decrypt(image)
assertEquals(44201, decrypted.size)
}
@Test
fun videoTest2() {
val myCipher =
AESGCM(
"373d19850ebc8ed5b0fefcca5cd6f27fde9cb6ac54fd32f6b4fad9d68ebe8ee0".hexToByteArray(),
"95e67b6874784a54299b58b8990499bd".hexToByteArray(),
)
val encrypted =
TestResourceLoader().loadFileData("trouble_video")
val decrypted = myCipher.decrypt(encrypted)
assertEquals(1277122, decrypted.size)
}
}
@@ -0,0 +1,37 @@
/*
* 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.nip17Dm
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import kotlinx.collections.immutable.persistentSetOf
import kotlin.test.Test
import kotlin.test.assertEquals
class ChatroomKeyTest {
@Test
fun testEquals() {
val k1 = ChatroomKey(persistentSetOf("Key1", "Key2"))
val k2 = ChatroomKey(persistentSetOf("Key1", "Key2"))
assertEquals(k1, k2)
assertEquals(k1.hashCode(), k2.hashCode())
}
}
Binary file not shown.
Binary file not shown.
@@ -23,8 +23,8 @@ package com.vitorpamplona.quartz
actual class TestResourceLoader {
actual fun loadString(file: String): String =
this@TestResourceLoader
.javaClass.classLoader!!
.getResourceAsStream(file)
.bufferedReader()
.use { it.readText() }
.javaClass.classLoader
?.getResourceAsStream(file)
?.bufferedReader()
?.use { it.readText() } ?: throw IllegalArgumentException("Resource not found: $file")
}
@@ -0,0 +1,23 @@
//
// Created by NullDev on 31/12/2025.
//
import Foundation
import RFC_3986
@objcMembers public class Rfc3986UriBridge: NSObject {
public func normalizeUrl(url: String) throws -> String {
let uri = try RFC_3986.URI(url)
let normalized = uri.normalized()
return normalized.value
}
public func isUrlValid(url: String) -> Bool {
return RFC_3986.isValidURI(url)
}
public func hostFromUri(url: String) throws -> String {
let actualUri = try RFC_3986.URI(url)
return actualUri.host!
}
}
@@ -0,0 +1,21 @@
//
// Created by NullDev on 20/01/2026.
//
import Foundation
@objcMembers public class UrlDetector: NSObject {
public func findURLs(text: String) -> [String] {
var links = [String]()
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
for match in matches {
guard let range = Range(match.range, in: text) else { continue }
let url = text[range]
links.append(String(url))
}
return links
}
}