Merge pull request #1757 from KotlinGeekDev/kmp-completeness
KMP Completeness, Part 1.
This commit is contained in:
Generated
-4
@@ -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
@@ -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
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -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)
|
||||
|
||||
|
||||
+44
@@ -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")
|
||||
}
|
||||
}
|
||||
+81
@@ -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)
|
||||
}
|
||||
}
|
||||
+37
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+23
-3
@@ -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)
|
||||
}
|
||||
|
||||
+257
-114
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+30
-8
@@ -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 <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> = <tt>position()</tt> is copied
|
||||
* to index zero, the byte at index <i>p</i> + 1 is copied
|
||||
* to index one, and so forth until the byte at index
|
||||
* <tt>limit()</tt> - 1 is copied to index
|
||||
* <i>n</i> = <tt>limit()</tt> - <tt>1</tt> - <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> <tt>></tt> <tt>remaining()</tt>,
|
||||
* then no bytes are transferred and an IllegalStateException is thrown
|
||||
* </p>
|
||||
*
|
||||
* <p> Otherwise, this method copies
|
||||
* <i>n</i> = <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> - <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> - <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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user