diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index b65547ca6..513727d82 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -6,10 +6,6 @@ - - diff --git a/gradle.properties b/gradle.properties index 71607cd40..8d72c802b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 \ No newline at end of file +kotlin.mpp.applyDefaultHierarchyTemplate=false + +# This is needed for the Swift bridge to work. +kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47bb25794..96bd8a885 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index befae0de9..c6c82b27e 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -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().configureEach { + environment("TEST_RESOURCES_ROOT", rootDir) + } + + tasks.withType().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) } } diff --git a/quartz/src/androidHostTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.android.kt b/quartz/src/androidHostTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.android.kt index 34edaf77c..bb522a8b7 100644 --- a/quartz/src/androidHostTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.android.kt +++ b/quartz/src/androidHostTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.android.kt @@ -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") } diff --git a/quartz/src/androidHostTest/resources/relayDB.txt b/quartz/src/androidHostTest/resources/relayDB.txt new file mode 100644 index 000000000..0977f44b3 --- /dev/null +++ b/quartz/src/androidHostTest/resources/relayDB.txt @@ -0,0 +1,2539 @@ +wss://nostr.wine +wss://relay.orangepill.dev +wss://xmr.usenostr.org +wss://nostr.portemonero.com +wss://nostr.xmr.rocks +wss://relay.nostr.band +wss://filter.nostr.wine +wss://nostr.milou.lol +wss://nostr.mutinywallet.com +wss://nostr-pub.wellorder.net +wss://nostr.zebedee.cloud +wss://nos.lol +wss://brb.io +wss://bitcoiner.social +wss://nostr.decentony.com +wss://relay.nostriches.org +wss://paid.spore.ws +wss://eden.nostr.land +wss://puravida.nostr.land +wss://5dzvuefllevkhk7miqynaviguedxfnofrayu2xwfwtlkdg4radjdlyqd.onion +wss://relay-jp.nostr.wirednet.jp +wss://relay.nostrich.land +wss://nostr.holybea.com +wss://nostr-relay.nokotaro.com +wss://nostr-paid.h3z.jp +wss://nostrja-kari.heguro.com +wss://nostr.mom +wss://nostr.fediverse.jp +wss://nostr.h3z.jp +wss://universe.nostrich.land +wss://nostr.uselessshit.co +wss://atlas.nostr.land +wss://relay.snort.social +wss://universe.nostrich.landlangenlanges +wss://nostr.slothy.win +wss://nostr.plebchain.org +wss://nostr-relay.untethr.me +wss://relay.nostr.com.au +wss://nostr.inosta.cc +wss://relay.nostrati.com +wss://nostr.bitcoiner.social +wss://relay.nostrplebs.com +wss://relay.nostr.info +wss://nostr-relay.wlvs.space +wss://nostr.oxtr.dev +wss://nostr.onsats.org +wss://relay.wellorder.net +wss://relay.plebstr.com +wss://no.str.cr +wss://nostr.walletofsatoshi.com +wss://nostr.mwmdev.com +wss://relay.nostr.bg +wss://nostr.rocks +wss://nostr.fmt.wiz.biz +wss://nostr.orangepill.dev +wss://nostr-pub.semisol.dev +wss://nostr.sandwich.farm +wss://relay.nostr.ch +wss://relay.orange-crush.com +wss://private.red.gb.net +wss://nostr.lnprivate.network +wss://nostr.lu.ke +wss://relay.nostr.wirednet.jp +wss://lightningrelay.com +wss://relay.nostrgraph.net +wss://relay.nostrica.com +wss://relay.mostr.pub +wss://nostr-sg.com +wss://nostr.zkid.social +wss://relay.nostr.vet +wss://relay.nostr3.io +wss://relay.arsip.my.id +wss://relay.current.fyi +wss://global.relay.red +wss://nostr.island.network +wss://node01.nostress.cc +wss://relay.nostr.net.in +wss://relay.utxo.one +wss://relay-1.arsip.my.id +wss://nostrical.com +wss://nostro.cc +wss://rsslay.nostr.moe +wss://relay.nostr.or.jp +wss://nostream.unift.xyz +wss://blastr.f7z.xyz +wss://relay.honk.pw +wss://universe.nostrich.landlangja +wss://nostream.ocha.one +wss://paid-relay.nost.love +wss://offchain.pub +wss://nostr-usa.ka1gbeoa21bnm.us-west-2.cs.amazonlightsail.com +wss://nostr.terminus.money +wss://nostr.shawnyeager.net +wss://public.nostr.swissrouting.com +wss://nostrex.fly.dev +wss://relay.727whisky.com +wss://relay.cryptocculture.com +wss://relay.bleskop.com +wss://nostr.lorentz.is +wss://nostr.actn.io +wss://nostr-relay.lnmarkets.com +wss://nostr.openchain.fr +wss://relay.punkhub.me +wss://nostr.blipme.app +wss://nostr.swiss-enigma.ch +wss://nostr-verified.wellorder.net +wss://at.nostrworks.com +wss://21sats.net +wss://e.nos.lol +wss://nostr.mikedilger.com +wss://nostr.azte.co +wss://noster.bitcoiner.social +wss://relay.nostr.moe +wss://nostream.nostrly.io +wss://nostr.gives.africa +wss://nostr.21sats.net +wss://bitcoinmaximalists.online +wss://paid.nostrified.org +wss://nostr.1sat.org +wss://nostr.relayer.se +wss://sg.qemura.xyz +wss://nostr.coinos.io +wss://nostr.bitcoinplebs.de +wss://nostrich.friendship.tw +wss://nostr.sg +wss://eosla.com +wss://nostr.sidnlabs.nl +wss://nostr.foundrydigital.com +wss://relay.nostrview.com +wss://nostr.semisol.dev +wss://relay.f7z.io +wss://wlvs.space +wss://nostr.v0l.io +wss://nostr-relay.digitalmob.ro +wss://rsslay.fiatjaf.com +wss://relay.theorangepillapp.com +wss://relay.zeh.app +wss://nostr.zoomout.chat +wss://relay.stoner.com +wss://nostr.cercatrova.me +wss://relay.ryzizub.com +wss://nostr-1.nbo.angani.co +wss://nostr21.com +wss://spore.ws +wss://nostrue.com +wss://no-str.org +wss://relay.taxi +wss://ragnar-relay.com +wss://relay.austrich.net +wss://relay.nostr-latam.link +wss://1.noztr.com +wss://relay.nostr.scot +wss://test.relay.nostrich.day +wss://jiggytom.ddns.net +wss://nostr.bongbong.com +wss://relay.nostromo.social +wss://relay.sendstr.com +wss://nostr-dev.universalname.space +wss://relay.nostrcheck.me +wss://nostr.libertasprimordium.com +wss://nostr.kollider.xyz +wss://expensive-relay.fiatjaf.com +wss://nostr-sandbox.minds.io +wss://relay.nostrich.de +wss://nostr.gromeul.eu +wss://relay.nostr.wine +wss://nostr.screaminglife.io +wss://nostr-relay.derekross.me +wss://nostrica.nostrnotes.com +wss://paid.no.str.cr +wss://nostr.sethforprivacy.com +wss://nostr.dumpit.top +wss://nostr.herci.one +wss://cheery-paddock-rsakdrtc35c55n6yregn.wnext.app +wss://nostr.blockpower.capital +wss://nostr.nym.life +wss://nostr-verif.slothy.win +wss://fiatdenier.com +wss://nostr.bitcoin-21.org +wss://nostr.fluidtrack.in +wss://nostr.developer.li +wss://r.ayit.org +wss://relay.nostr.nu +wss://nostr.bostonbtc.com +wss://rly.social +wss://nostr.bridgey.dev +wss://relay.nostrprotocol.net +wss://nostr.mado.io +wss://nostr.einundzwanzig.space +wss://nostr2.actn.io +wss://nostr-relay.freedomnode.com +wss://nostr.pleb.network +wss://nostr.mouton.dev +wss://eelay.current.fyi +wss://nostr.notmyhostna.me +wss://nostr.pjv.me +wss://nostr.jatm.link +wss://nostr.fractalized.ovh +wss://nostr-relay.app.ikeji.ma +wss://relayer.ocha.one +wss://nostr.com.de +wss://nostr-2.afarazit.eu +wss://nostr.l00p.org +wss://nostr.drss.io +wss://relay.nostrify.io +wss://nostr.radixrat.com +wss://nostr-relay.bitcoin.ninja +wss://nostrsatva.net +wss://nostr.mustardnodes.com +wss://nostr01.vida.dev +wss://nostr.noones.com +wss://nostr.easify.de +wss://nostr3.actn.io +wss://moonbreeze.richardbondi.net +wss://nostr.naut.social +wss://private-nostr.v0l.io +wss://nostr.zaprite.io +wss://nostr.lightninglinks.xyz +wss://nostr.hackerman.pro +wss://nr.yay.so +wss://nostr.roundrockbitcoiners.com +wss://nostr.sovbit.host +wss://nostrelay.yeghro.site +wss://pow32.nostr.land +wss://nostr.1729.cloud +wss://nostr.rdfriedl.com +wss://nostr.h4x0r.host +wss://nostr.up.railway.app +wss://nostr.lnorb.com +wss://nostr.lordkno.ws +wss://relay.nostr.vision +wss://nostr-3.orba.ca +wss://satstacker.cloud +wss://freedom-relay.herokuapp.com +wss://nostr-relay.freeberty.net +wss://nostr.unknown.place +wss://nostr.delo.software +wss://relay.nostr.pro +wss://relay.minds.com +wss://nostr.ono.re +wss://relay.grunch.dev +wss://relay.cynsar.foundation +wss://relay.oldcity-bitcoiners.info +wss://relay.bitid.nz +wss://relay.nostr.xyz +wss://relay.futohq.com +wss://relay.farscapian.com +wss://astral.ninja +wss://relay.sovereign-stack.org +wss://nostr-2.zebedee.cloud +wss://nostr.nymsrelay.com +wss://relay.kronkltd.net +wss://relay.r3d.red +wss://universe.nostrich.landlangen +wss://nostr-dev.wellorder.net +wss://nostr.beta3.dev +wss://nostr.data.haus +wss://nostr.hugo.md +wss://relay-dev.cowdle.gg +wss://relay.dwadziesciajeden.pl +wss://tmp-relay.cesc.trade +wss://nostr.massmux.com +wss://relay.nostr.africa +wss://nostr1.tunnelsats.com +wss://nostr.f44.dev +wss://relay.n057r.club +wss://nostr-verif.slothy.com +wss://nostr.1f52b.xyz +wss://nostr.sebastix.dev +wss://nostr.lightning.contact +wss://nostr.rly.social +wss://noster.online +wss://relay.lexingtonbitcoin.org +wss://nostr.bitcoinbay.engineering +wss://nostr.howtobitcoin.shop +wss://blg.nostr.sx +wss://deschooling.us +wss://foolay.nostr.moe +wss://freespeech.casa +wss://nostr-01.bolt.observer +wss://nostr-01.dorafactory.org +wss://nostr-au.coinfundit.com +wss://nostr-eu.coinfundit.com +wss://nostr-relay.alekberg.net +wss://nostr-pub1.southflorida.ninja +wss://nostr-relay.gkbrk.com +wss://nostr-relay.pcdkd.fyi +wss://nostr-relay.schnitzel.world +wss://nostr-us.coinfundit.com +wss://nostr.21crypto.ch +wss://nostr.600.wtf +wss://nostr.8e23.net +wss://nostr.app.runonflux.io +wss://nostr.arguflow.gg +wss://nostr.bch.ninja +wss://nostr.chainofimmortals.net +wss://nostr.cizmar.net +wss://nostr.cheeserobot.org +wss://nostr.coollamer.com +wss://nostr.corebreach.com +wss://nostr.cro.social +wss://nostr.easydns.ca +wss://nostr.globals.fans +wss://nostr.handyjunky.com +wss://nostr.itas.li +wss://nostr.sectiontwo.org +wss://nostr.spleenrider.one +wss://nostr.thibautrey.fr +wss://nostr.uthark.com +wss://nostr.vulpem.com +wss://nostr.w3ird.tech +wss://nostr.whoop.ph +wss://nostr.yuv.al +wss://nostr01.opencult.com +wss://nostre.cc +wss://nostream.denizenid.com +wss://nostring.deno.dev +wss://pow.nostrati.com +wss://relay-pub.deschooling.us +wss://nostr.jiashanlu.synology.me +wss://nostr.klabo.blog +wss://relay.valireum.net +wss://nostr.fly.dev +wss://nostr.nordlysln.net +wss://nostr.zerofeerouting.com +wss://rsslay.nostr.net +wss://nostr-relay.nonce.academy +wss://nostr.rewardsbunny.com +wss://lv01.tater.ninja +wss://nostr-2.orba.ca +wss://nostr.orba.ca +wss://nostr.supremestack.xyz +wss://nostrrelay.com +wss://relay.nostr.au +wss://nostr.oooxxx.ml +wss://nostr.yael.at +wss://nostr-relay.trustbtc.org +wss://nostr.namek.link +wss://nostr-relay.wolfandcrow.tech +wss://nostr.satsophone.tk +wss://relay.dev.kronkltd.net +wss://nostr2.namek.link +wss://relay.21spirits.io +wss://relay.minds.io +wss://nostr.d11n.net +wss://nostr.tunnelsats.com +wss://nostr.leximaster.com +wss://mule.platanito.org +wss://nostr.robotechy.com +wss://relay.nostrmoto.xyz +wss://relay.boring.surf +wss://nostr.gruntwerk.org +wss://nostr.hyperlingo.com +wss://nostr.ethtozero.fr +wss://nostr.nodeofsven.com +wss://nostr.jimc.me +wss://nostr.utxo.lol +wss://relay.nyx.ma +wss://nostr.shmueli.org +wss://wizards.wormrobot.org +wss://nostr.sovbit.com +wss://nostr.datamagik.com +wss://relay.nostrid.com +wss://nostr1.starbackr.me +wss://relay.nostr.express +wss://nostr.formigator.eu +wss://nostr.xpersona.net +wss://nostr.digitalreformation.info +wss://nostr-relay.usebitcoin.space +wss://nostr-alpha.gruntwerk.org +wss://nostr-relay.australiaeast.cloudapp.azure.com +wss://nostr-relay.smoove.net +wss://nostr-relay.j3s7m4n.com +wss://nostr.demovement.net +wss://nostr.thesimplekid.com +wss://nostr.aozing.com +wss://nostr.blocs.fr +wss://no.str.watch +wss://btc.klendazu.com +wss://nostr.mrbits.it +wss://nostr.zenon.wtf +wss://no.contry.xyz +wss://nostream.gromeul.eu +wss://relay.nostr.ro +wss://nostr.ncsa.illinois.edu +wss://nostr.itssilvestre.com +wss://nostr.chaker.net +wss://knostr.neutrine.com +wss://nostr.pobblelabs.org +wss://nostr.simatime.com +wss://relay.nosphr.com +wss://student.chadpolytechnic.com +wss://nostr.localhost.re +wss://nostr.coinsamba.com.br +wss://deconomy-netser.ddns.net:2121 +wss://nostr.21m.fr +wss://zur.nostr.sx +wss://nostr-relay.texashedge.xyz +wss://spleenrider.herokuapp.com +wss://nostr.bitcoin.sex +wss://relay.nostrzoo.com +wss://nostr.blockchaincaffe.it +wss://nostr-bg01.ciph.rs +wss://knostr.neutrine.com:8880 +wss://nostr.ahaspharos.de +wss://nostr.argdx.net +wss://nostr.snblago.com +wss://merrcurr.up.railway.app +wss://nostr.bingtech.tk +wss://relay.nostr.wf +wss://relay.koreus.social +wss://nostr.randomdevelopment.biz +wss://relay.nostr.hu +wss://relay.nostr.lu +wss://relay.nostr.ae +wss://middling.myddns.me:8080 +wss://nostr.nikolaj.online +wss://relay.nostrology.org +wss://nostr.satoshi.fun +wss://nostream.kinchie.snowinning.com +wss://nostr.lapalomilla.mx +wss://relay.thes.ai +wss://rsr.uyky.net:30443 +wss://nostrafrica.pcdkd.fyi +wss://nostr.bitcoin-basel.ch +wss://relay.21baiwan.com +wss://nostr.ddns.net:8008 +wss://free-relay.nostrich.land +wss://nostr.lukeacl.com +wss://nostr.ddns.net +wss://nostr.rocket-tech.net +wss://nostr-1.afarazit.eu +wss://nostr.0nyx.eu +wss://nostr-mv.ashiroid.com +wss://lbrygen.xyz +wss://nostr.community.networks.deavmi.assigned.network +wss://nostr.ownscale.org +wss://relay1.gems.xyz +wss://nostr.soscary.net +wss://nostr.0xtr.dev +wss://damus.io +wss://relay.alien.blue +wss://nostr.btcmp.com +wss://relayer.fiatjaf.com +wss://relay.lacosanostr.com +wss://adult.18plus.social +wss://nostrrr.bublina.eu.org +wss://relay.stoner +wss://nostr.pwnshop.cloud +wss://nostr.directory +wss://nostr-relay-dev.wlvs.space +wss://member.cash +wss://relay.nyc1.vinux.app +wss://nostr-relay.digitamob.ro +wss://nor.st +wss://nostr.topeth.info +wss://nostr.rocketstyle.com.au +wss://relay.tnano.duckdns.org +wss://nostr.21l.st +wss://electra.nostr.land +wss://relay.codl.co +wss://nostr.koning-degraaf.nl +wss://relay.mrjohnsson.net +wss://nostr.thank.eu +wss://relay.stonez.me +wss://relay.nostr.distrl.net +wss://relay.valera.co +wss://api.semisol.dev +wss://nostr.lol +wss://relay.shitforce.one +wss://n-word.sharivegas.com +wss://lamp.wtf +wss://nostr.bitcoinpuertori.co +wss://nostr-01.bolt.oberver +wss://3d515c5277e9.ngrok.io +wss://nostr.xmrk.mooo.com +wss://alphapanda.pro +wss://relays.world +wss://universe.nostrich.landlangzh +wss://arnostr.permadao.io +wss://relay.chenxixian.cn +wss://universe.nostrich.landlangzhlangen +wss://v2r.chenxixian.cn +wss://nostr-relay-test.nokotaro.work +wss://universe.nostrich.landlangjalangen +wss://nostr.risa.zone +wss://relay.nosbin.com +wss://translate.argosopentech.com +wss://edennostr.land +wss://nostr.kawagarbo.xyz +wss://nostr.member.cash +wss://ch1.duno.com +wss://nostream-production-b80e.up.railway.app +wss://relay1.nostrich.cloud +wss://relay.t5y.ca +wss://nostr.zhongwen.world +wss://nostr.p2sh.co +wss://nostr.thomascdnns.com +wss://nostream.simon.snowinning.com +wss://relay.nostr.blockhenge.com +wss://nostr.buythisdip.com +wss://nostrua.com +wss://relay.bigred.social +wss://lingoh.dev +wss://nostr.poster.place +wss://nostr.geekgalaxy.com +wss://oarnx6xdrq5mygfdrbmzsvh3is3holefpz2x4qwbopwcicwd63gcivid.onion +wss://relay.nostropolis.xyz +wss://nostream-production-ba43.up.railway.app +wss://nostr.sabross.xyz +wss://relay.nvote.co +wss://nostrati.com +wss://cloudnull.land +wss://nostr.frennet.xyz +wss://nostr.wine.com +wss://nostr.sactiontwo.org +wss://nostr.liberty.fans +wss://nostr.primz.org +wss://btc-italia.online +wss://homenode.local:4848 +wss://nostr.frennet.xyzl +wss://relay.roosoft.com +wss://rasca.asnubes.art +wss://nostr.bitcoin.sexanewlycre +wss://nostr.barf.bz +wss://nostr.middling.mydns.jp +wss://relay.xuzmail.com +wss://no-str.wnhefei.cn:28443 +wss://quirky-bunch-isubghsvoi26fbbt3n7o.wnext.app +wss://nostr.fennel.org:7000 +wss://nostr.0ne.day +wss://nostr.vpn1.codingmerc.com +wss://nostr.jacany.com +wss://nostream.lucas.snowinning.com +wss://relay.beta.fogtype.com +wss://nostr.zue.news +wss://nostream.madbean.snowinning.com +wss://nostr2.rbel.co +wss://relay.1bps.io +wss://nostream-relay-nostr.831.pp.ua +wss://zee-relay.fly.dev +wss://nostrrelay.geforcy.com +wss://relay.nostr.jhot.me +wss://nostr.itredneck.com +wss://nostr.h3y6e.com +wss://relay.bitcoiner.social +wss://hos.lol +wss://iris.to +wss://nostr-pub.senisol.dev +wss://nostr-pub.wellirder.net +wss://bitcoinforthe.lol +wss://relav.nostr.info +wss://3e32-200-229-144-129.ngrok.io +wss://nostr-relay.hzrd149.com +wss://nostr-world.h3z.jp +wss://nostr.thesamecat.io +wss://nostr.compile-error.net +wss://relayable.org +wss://mostra.milou.lol +wss://nproxy.cc +wss://nostr.bitmatk.io +wss://coracle.social +wss://umbrel.local:4848 +wss://nostr.ownbtc.online +wss://wss.nostrgram.co:444 +wss://nostr.minimue81.selfhost.co +wss://relay.current.fy +wss://nostream.nostr.parts +wss://nostr.zebede.cloud +wss://nostrwhoop.ph +wss://relay.nostrified.org +wss://nproxy.zerologin.co +wss://nostr.pcdkd.fyi +wss://relay.kongerik.et +wss://nostr.eden.land +wss://nostr.retroware.run.place +wss://relay.humanumest.social +wss://bhagos.org +wss://hushvault.ie +wss://nostream-production-9458.up.railway.app +wss://nostr.nakamotosatoshi.cf +wss://globals.fans +wss://nostr.cruncher.com +wss://nostr.global.fan +wss://relay.nostr.snblago.com +wss://nostr.nokotaro.com +wss://ostr-1.afarazit.eu +wss://relay.zhix.in +wss://stats.nostr.band +wss://nostr.fine +wss://vxlw4rlg7go34ol43g4gxbvfu4txdzjauquvnbptzwjflezs3vik55id.onion +wss://big.fist.black +wss://universe.nostrich.landlangzhlangja +wss://relay.plebz.space +wss://nostrich.land +wss://relay.mynostr.fun +wss://swiss-enigma.ch +wss://nostr1.current.fyi +wss://relay.atlas.nostr.land +wss://nostr.band +wss://n.wingu.se +wss://nostr.jmdtx.com +wss://nostrproxy-1.f7z.io +wss://nostr.ch +wss://roundrockbitcoiners.com +wss://nostr.sept.ml +wss://srelay.roli.social +wss://nostr.monostr.com +wss://nostr.dojotunnel.online +wss://nostrica.dojotunnel.online +wss://nostr.shadownode.org +wss://thes.ai +wss://rsslay.wss +wss://nostr.vol.io +wss://nostrgram.co +wss://habla.news +wss://runningnostr.lol +wss://mostr.pub +wss://relay.example2.com +wss://profiles.f7z.io +wss://nostr.adpo.co +wss://jp-relay-nostr.invr.chat +wss://nostr.anchel.nl +wss://mutinywallet.com +wss://relay.nostrbr.online +wss://filter.eden.nostr.land +wss://relay.nostrdocs.com +wss://relay.nostr.lucentlabs.co +wss://n.xmr.se +wss://nostr.relayer.rs +wss://monad.jb55.com:8080 +wss://nostr.watch +wss://universe.nostrich.landlangenlangzhlangja +wss://nostr.asdf.mx +wss://ts.relays.world +wss://arc1.arcadelabs.co +wss://stealth.wine +wss://nostr.bg +wss://really.nostr.bg +wss://realy.nostr.bg +wss://relai.nostr.bg +wss://relay-verified.deschooling.us +wss://4.up.railway.app +wss://nostr-relay.aapi.me +wss://nostr-z9tc.onrender.com +wss://nostrich.site +wss://nostr.ginuerzh.xyz +wss://310b-200-229-144-129.ngrok.io +wss://nostr.aste.co +wss://black.nostrcity.club +wss://nostr.guru +wss://nostrica.com +wss://relay.leesalminen.com +wss://nostr.shroomslab.net +wss://meta-relay-beta.nostr.wirednet.jp +wss://nostr.reamde.dev +wss://nostr.africa +wss://powrelay.xyz +wss://rsslay.data.haus +wss://nostr.danvergara.com +wss://nostr.one.re +wss://dev2.hazilitt.fiatjaf.com +wss://nostr-2.zebdeee.cloud +wss://zerosequioso.com +wss://brb.io.relay +wss://relay.realsearch.cc +wss://nodestr.fmt.wiz.biz +wss://r.alphaama.com +wss://nostr.relay-wlvs.space +wss://relay.21spiritis.io +wss://nostr.pinkanki.org +wss://nostr.damus.io +wss://fin-nostr.seekdisruption.com +wss://relay.nostr.lu.ke +wss://nostr.rdfried.com +wss://nostr.trustbtc.org +wss://nostr.verymad.net +wss://relay.damus.info +wss://relays.nostrplebs.com +wss://nostr.688.org +wss://15171031.688.org +wss://dgi4mb7antpcmrx4rynm6xq52xzt5duvxa4iwucq4mszgpz6smrjajqd.onion +wss://mastodon.cloud +wss://nostr.ch3n2k.com +wss://nostr.forecastdao.com +wss://nostr.nostrelay.org +wss://nostr.robotesc.ro +wss://nostr.test.aesyc.io +wss://nostr.web3infra.xyz +wss://nostrrelay.maciejz.net +wss://nostrsxz4lbwe-nostr.functions.fnc.fr-par.scw.cloud +wss://nostrpurple.com +wss://rsslay.ch3n2k.com +wss://nos.qghs.in +wss://nostr.nordlysln.net:3241 +wss://nostr.net.in +wss://relay.rip +wss://universe.nostrich.landlangenlangja +wss://wmv-vm.local:4848 +wss://relay.austritch.net +wss://relay.oxtr.dev +wss://rwlay.bigred.social +wss://relay.lexongtonbitcoin.org +wss://knostr.neutrine +wss://nostr.online +wss://filter.stealth.wine +wss://nostream-production-5895.up.railway.app +wss://nostr.stereosteve.com +wss://relay01.apus.network +wss://test.nostr.0x50.tech +wss://nostr.0x50.tech +wss://nostr.256k1.dev +wss://nostr.malin.onl +wss://jqiwgflfw4dezjsy42frompmknrlcfazoiyngftgknj7yrmnhtobd7id.local +wss://b.ayit.org +wss://nostrelay.rajabi.ca +wss://off20chain.pub +wss://nostr.milou.land +wss://nostr.primedomain.fr +wss://nostr.theblockreward.com +wss://anon.computer +wss://relay.hamnet.io +wss://nostramsterdam.vpx.moe +wss://global-relay.cesc.trade +wss://btcpay.kukks.org +wss://relay.cent2sat.com +wss://nostr.mnethome.de +wss://nostream.sh4.red +wss://klockenga.social +wss://nostream.megadope.snowinning.com +wss://nostr.cvilleblockchain.org +wss://nostr.bitocial.xyz +wss://nostr.bitcoiner.socail +wss://nostr-relay.eniehack.net +wss://greenart7c3.dedyn.io +wss://nostr.data.naus +wss://8.tcp.ngrok.io:19607 +wss://relay.nostr24.com +wss://d463rbo7dgbfuxvvxpory2og2etl4gttfzmqcixdq7rpts47lpgolkyd.onion +wss://dublin.saoirse.dev +wss://www.weixin.com +wss://nostr-rs-relay.cryptoassetssubledger.com +wss://nostr.kojira.net +wss://nostr.fan +wss://nostr.pk +wss://getalpy.com +wss://billert.xyz +wss://circle-ay.info +wss://jawsh.xyz +wss://walletofsatoshi.com +wss://ogblock.xyz +wss://nodestrich.com +wss://kunigaku.gith +wss://nostr.21ideas.org +wss://133332.xyz +wss://asats.io +wss://nostrchack.me +wss://chalow.net +wss://cashu.me +wss://tsukemonogit.git +wss://h3y6e.com +wss://elder.nostr.land +wss://xmr.rocks +wss://nostr.build +wss://mofumemo.com +wss://kpherox.dev +wss://sb.nostr.band +wss://tyiu.xyz +wss://welkinhere.githu +wss://ocha.one +wss://in.tips +wss://vitorpamplona.co +wss://fiatjaf.com +wss://akiomik.github.io +wss://stacker.news +wss://nvk.org +wss://lordkno.ws +wss://nostr.indus +wss://lotdkno.ws +wss://nosutora.com +wss://milou.lol +wss://mostr.pu +wss://dergigi.com +wss://shirehodl.com +wss://cash.app +wss://h3z.jp +wss://murachue.cytes.net +wss://snowcait.gith +wss://jb55.com +wss://nodeless.io +wss://orange-crush.com +wss://nostr.com.au +wss://oooxxx.ml +wss://plebs.place +wss://ahr999.com +wss://penpenpng.github.io +wss://nostrpurple.co +wss://thank.eu +wss://weep.jp +wss://tigerville.no +wss://nostrcheck.me +wss://frenstr.com +wss://ln.tips +wss://ryumu.dev +wss://honeyroad.store +wss://harlembitcoin.com +wss://f7z.io +wss://relay.fan +wss://nisshiee.org +wss://www.lopp.net +wss://getalby.com +wss://heguro.com +wss://wil.bio +wss://b.tc +wss://nodedttich.com +wss://relay.taldra.in +wss://relat.nostrica.com +wss://nostr.boring.surf +wss://nostr.raitisoja.net +wss://nostr.astrox.app +wss://nostr.mjex.me +wss://slick.mjex.me +wss://nostr.hrmb.org +wss://relay.semaphore.life +wss://rss.nostr.band +wss://63ragcfwb5xhoe5gfflazfyrde3qjdo73cblhhmnbviizowdo2q5haid.onion:5051 +wss://nostrblip.app +wss://relay.vanderwarker.family +wss://relay-local.cowdle.gg +wss://relay.damus.com +wss://relay.reeve.cn +wss://relay.strfry.net +wss://relay.alxgsv.com +wss://nostrv0l.io +wss://relay.nostrula.com +wss://release.nostr.band +wss://nostr.k3tan.com +wss://nostr.bitcoin.social +wss://thesimplekid.space +wss://relay.ypcloud.com +wss://at.nostrwork.at +wss://nostream.dev.kronkltd.net +wss://nostr-relay.xbytez.io +wss://relay.nostr.io +wss://relay.nvote.co:443 +wss://relay.cryptoculture.com +wss://rjj6ejkihilniytxs56qrgtttgcfnnjvbii6vaas6jzppcmekd63ugad.local +wss://puravida.nostr.land.com +wss://bitcoinmaximalist.online +wss://wine.nostr +wss://nostr.messagepush.io +wss://nostrich.love +wss://relaynostrplebs.com +wss://hamstr.to +wss://yosupp.app +wss://snort.social +wss://relay.nostr.gt +wss://nost.ratchat.nl +wss://nostr.chrissmith.site +wss://i.relay.boats +wss://nostr.wineto +wss://nostr.eluc.ch +wss://nostrplebs.com +wss://nostr.tools.global.id +wss://nostr.rocketnode.space +wss://relay.roli.social +wss://bitcoin.nostr.com +wss://relay.badgr.space +wss://nostriches.club +wss://nostr-check.me +wss://nostrelay.nokotaro.com +wss://rbr.bio +wss://rly.bopln.com +wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.onion +wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.local +wss://20nos.lol +wss://test.theglobalpersian.com +wss://nostr.exposed +wss://nostr-pub.liujiale.me +wss://nostream.frank.snowinning.com +wss://nstrs.fly.dev +wss://eospark.com +wss://relay.nosterplebs.com +wss://nproxy.kristapsk.lv +wss://nostr.universalname.space +wss://relays.snort.social +wss://nostr.how +wss://kukks.org +wss://nostr.dutch.cryptonews +wss://relay.cryptojournaal.net +wss://relay.dutch.cryptonews +wss://nostr.cryptojournaal.net +wss://no-str.wnhefei.cn +wss://www.131.me +wss://rsr.uyky.net +wss://nostr-relay.ie9.org +wss://nostr.fmt.wis.biz +wss://nostr.exotr.dev +wss://nostr.merrcurr.com +wss://realy.damus.io +wss://nerostr.xmr.rocks +wss://rkdgwzgvcgrciemlnfsxqgyrv5whgpw44s6zycokmuchpq4ucflgjtqd.local +wss://nostr.simplex.icu +wss://ralay.damus.io +wss://relay.kronkitd.net +wss://nostr.info +wss://nostr.lnnodeinsight.com +wss://nostr.truckenbucks.com +wss://brt.io +wss://nostr.lingoh.dev +wss://relay.nor.st +wss://nostr-relay.inmarkets.com +wss://wiz.biz +wss://nostream.git +wss://nostr.dpbu.de +wss://wcl2meyp236fa3dmfzfyq6aacbdoixrlocb6zozjs6xklxizschj2did.local +wss://relay.nostr.co.jp +wss://relap.orzv.workers.dev +wss://nostr.bitcoiner.socia +wss://eden.nosrt.land +wss://strfry.cryptocartel.social +wss://nostr.citizenry.technology +wss://universe.nostrich.landlang +wss://nostr.inprivate.network +wss://relay.snort.test +wss://lpkue6jtz3pnp7zok4jwlct4n3mzuffsfrpagiffsuplyaswhdtmpoid.local +wss://nostr.rezhajulio.id +wss://lnbits.eldamar.icu +wss://nostr.freefrom.fi +wss://yael.at +wss://rain8128.github.io +wss://badges.page +wss://latam1-nostr.stealthy.co +wss://nostr.relay.se +wss://nfdn.testnet.dotalgo.io +wss://relav.nostr3.io +wss://ofchain.pub +wss://nostr.fmt.wiz.bi +wss://relay.gui.dog +wss://eden.nostry.land +wss://nostr.relay.limo +wss://relay.nostrichs.org +wss://20nostr.mom +wss://relay20damus.io +wss://nostr.sandwich.pro +wss://sq.qemura.xyz +wss://nostr-pub.seminol.dev +wss://nostr.eunundzwanzig.space +wss://nostr.bitcoinet.social +wss://nostr-relay.untether.me +wss://relay.nostr.pub +wss://nostr.100p.org +wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.local +wss://nostr.gram +wss://nostr.videre.net +wss://nostr-2.zebedee.cloudwss +wss://nostr-pub.wellorder.netwss +wss://sonzai.net +wss://snort.fail +wss://ppavybjpqjft5slnpeovehbegomhwtvvxtesvwwdrfndz6qe2c5kf2ad.local +wss://nostr.land +wss://relay.nvote.com +wss://purevida.nostr.land +wss://nostr.bitcoiner.soical +wss://nostr.inosta.co +wss://nas.lol +wss://wss.nostr.milou +wss://nostr.cheesebot.org +wss://eyeswideshut.ath.cx +wss://test.relays.world +wss://relay.snort.com +wss://relay.nostrplebs.co +wss://nostr.kimi.im +wss://nostrum.com +wss://wss.nostr.wine +wss://relay.usenostr.org +wss://paladium.my:4848 +wss://umbrel.home.local:4848 +wss://nostr.metamadeenah.com +wss://lnbits.sdbtc.org +wss://relay.nostr.mutinywallet.com +wss://relay.theglobalpersian.com +wss://relay1.easymeta.app +wss://relay.nostris.online +wss://s1.wonder3.org +wss://eden.nostraland +wss://byc.klendazu.com +wss://ephemerelay.mostr.pub +wss://7si6co27cvaw5yjyx6asvxfmaw5ah2arywwgrem4y5svi5ntskoeb5id.onion +wss://nwmdev.com +wss://nostro.online +wss://nostr.massimux.com +wss://nostr.glate.ch +wss://nostr.openordex.org +wss://nostr.schorsch.fans +wss://nostr-relay-dev.nisshiee.org +wss://ibz.me +wss://alexandernostrplebs.com +wss://fishbanananostrplebs.com +wss://relay.nostrcheck.com +wss://nostr.roli.io +wss://nostr.net.za +wss://nostr.worldkey.io +wss://nostr-pub.welloorder.net +wss://nostr.actin.io +wss://nostrzebedee.cloud +wss://nostr.zclub.app +wss://nostr.13x.sh +wss://nostr.totient.xyz +wss://nostr1.federated.computer +wss://nostr.zhix.in +wss://nostr.vdstruis.com +wss://caro-relay.fiatjaf.com +wss://nostr.winewss +wss://notstro.wine +wss://nostr.btc-library.com +wss://nostr.phenomenon.space +wss://nostr.octr.dev +wss://nostr.impervious.live +wss://nostr.plebs.space +wss://iefan.tech +wss://w3ird.tech +wss://yunginter.net +wss://nostr.coach +wss://sleepy.cafe +wss://freespeechextremist.com +wss://nostr.uselessshit.com +wss://liberdon.com +wss://eveningzoo.club +wss://gleasonator.com +wss://mindly.social +wss://ottawa.place +wss://noagendasocial.com +wss://poa.st +wss://misskey.io +wss://misskey.cf +wss://nostr.bybieyang.com +wss://mastodon.online +wss://toad.social +wss://best-friends.chat +wss://universeodon.com +wss://mastodon.world +wss://mastodon.social +wss://nostr.vulpem +wss://nicecrew.digital +wss://front-end.social +wss://nostr-relay.wellorder.net +wss://relays.pro +wss://rsslay.sovbit.host +wss://seal.cafe +wss://social.6bq.de +wss://universe.nostrich.landlangjalangzh +wss://social.xenofem.me +wss://mi.hibi-tsumo.com +wss://mstdn.social +wss://mas.to +wss://relay.rebelbase.site +wss://pixelfed.social +wss://digitalcourage.social +wss://ruby.social +wss://cr8r.gg +wss://nijimiss.moe +wss://returtle.com +wss://lor.sh +wss://toot.community +wss://mstdn.jp +wss://relay.berserker.town +wss://nostr.rajabi.ca +wss://fosstodon.org +wss://misskey.takehi.to +wss://izj3isbk3pmade74ontdijodhehsytnw2iokdhh6k3flk4mq2pau6sid.onion +wss://mastodon.scot +wss://nostriches.org +wss://aus.social +wss://relay.nostr.amane.moe +wss://romancelandia.club +wss://stonez.me +wss://merrcurr.com +wss://nostr.ist +wss://onprem.wtf +wss://fedibird.com +wss://s2.wonder3.org +wss://freespech.casa +wss://disabled.social +wss://relay.house +wss://nostr-pub.welllorder.net +wss://social.teamb.space +wss://infosec.exchange +wss://blockedur.mom +wss://progressivecafe.social +wss://mstdn.ca +wss://c.im +wss://mefi.social +wss://basebitcoinplebs.place +wss://med-mastodon.com +wss://ohai.social +wss://defcon.social +wss://pxlmo.com +wss://zlocur7ctbds4qsdswb3qpkp6n2e2ywqne2fp2tdn4fqubpudfiyxwid.local +wss://thebag.social +wss://mstdn.party +wss://relay.serpae.xyz +wss://clew.lol +wss://qoto.org +wss://relay.exchange +wss://nostr.relayable.org +wss://mastodon.coffee +wss://kmy.blue +wss://home.social +wss://detmi.social +wss://brighteon.social +wss://nostr.nom +wss://theblower.au +wss://astral.nostr.land +wss://beige.party +wss://orangepill.dev +wss://redgreenblue.click +wss://nostr.fredix.xyz +wss://nostr.essydns.ca +wss://nostr.private.network +wss://nostr.member.cas +wss://nostr-rely.digitalmob.ro +wss://relay.nostr +wss://relay.current +wss://no-str.or +wss://pl.gamers.exposed +wss://studentchadpolytechnic.com +wss://scicomm.xyz +wss://relay.alien-sos.gov +wss://masto.es +wss://spinster.xyz +wss://parallels-parallels-virtual-platform.local:4848 +wss://relay.daums.io +wss://kolektiva.social +wss://mastodonapp.uk +wss://convo.casa +wss://sfba.social +wss://techhub.social +wss://leafposter.club +wss://nrw.social +wss://mastodon.uno +wss://handon.club +wss://social.vivaldi.net +wss://nostr.goller.net +wss://minazukey.uk +wss://mstdn.beer +wss://expressional.social +wss://paid.nostr.0x50.tech +wss://mstdn.nere9.help +wss://relay.nostr.ai +wss://relay.noswss +wss://relay.nwss +wss://furry.engineer +wss://uselessshit.co +wss://kosmos.social +wss://mynostr.io +wss://premis.one +wss://social.tchncs.de +wss://retro.pizza +wss://pearl-mount-showed-fishing.trycloudflare.com +wss://kpa4k6acxzjv2m2p72keftbpaymwpq2h67jqnin3d4y3djxyheuifoqd.onion +wss://gm7.social +wss://relays.nostr.info +wss://chitter.xyz +wss://hackers.town +wss://anarchism.space +wss://relay.nostrica +wss://halifaxsocial.ca +wss://asimon.org +wss://nostr.blipme.add +wss://troet.cafe +wss://octodon.social +wss://m.cmx.im +wss://filename-ambassador-distance-mountains.trycloudflare.com +wss://nostr.getgle.org +wss://relay.froth.zone +wss://novoa.nagoya +wss://relay.dispute.systems +wss://clubcyberia.co +wss://thechimp.zone +wss://coolsite.win +wss://puravida.nostra.land +wss://shelter.local:1111 +wss://det.social +wss://nostr-2.zebee.cloud +wss://chaosfem.tw +wss://nostr.v01.io +wss://social.kechpaja.com +wss://mastodon.green +wss://plebchain.nostr +wss://plebchain.nostr.land +wss://relay.onsats.org +wss://u5epuanp2fbie4phw6zekzna6zotvsffji4td4ee7iwgwdxlz4kwqqad.onion:5051 +wss://3ddc8bbee6db.ngrok.app +wss://b4968f09859e.ngrok.io +wss://genserver.social +wss://masto.ai +wss://hachyderm.io +wss://shitpost.cloud +wss://oldbytes.space +wss://mastodon.ie +wss://baraag.net +wss://nostr.dvdt.dev +wss://relay.com.de +wss://nostr.rbel.co +wss://bologna.one +wss://nostr-relay.app +wss://kagamisskey.com +wss://nostr-rs-relay.phamthanh.me +wss://nostr.bcmp.com +wss://blogstack.io +wss://rly.nostrkid.com +wss://mastodon.bida.im +wss://freeatlantis.com +wss://pieville.net +wss://climatejustice.rocks +wss://relay.mynostr.id +wss://relay.farscapian +wss://relay.blogstack.io +wss://orwell.fun +wss://misskey.04.si +wss://sushi.ski +wss://nostr.milou.lo +wss://pawoo.net +wss://tooter.social +wss://mastodon.sdf.org +wss://nostr.com +wss://misskey.design +wss://macaw.social +wss://relay.nostr.inforelay.nostr.band +wss://pub1.southflorida.ninja +wss://strawberry-pudding.net +wss://mastodon-japan.net +wss://nostream.0x50.tech +wss://multiplextr.coracle.social +wss://pleroma.skyshanty.xyz +wss://uxxq6b2enojvflhkrzsg4erakd5rrb7v2cql4m4pspj4xtqwyli47rid.local +wss://masto.deoan.org +wss://replay.damus.io +wss://melhorque.com.br +wss://nostr1.actn.io +wss://nostr.bolt.fun +wss://relay.vtbmoyu.com +wss://eosla.comrelay.zeh.app +wss://bikeshed.party +wss://nostr.xanny.family +wss://milker.cafe +wss://nostr.give.africa +wss://relay.got-relayed.com +wss://cum.salon +wss://puravid.nostr.land +wss://soc.punktrash.club +wss://seafoam.space +wss://h4.io +wss://nostr.swiss.enigma.ch +wss://toot.cafe +wss://sneed.social +wss://newsie.social +wss://indg.club +wss://xoxo.zone +wss://relay.nostr.bitcoiner.social +wss://relay.snort.band +wss://socel.net +wss://social.coop +wss://postpandemicparty.org +wss://merveilles.town +wss://search.nostr.wine +wss://search.nos.today +wss://mstdn-huahin.com +wss://eden.nostr.la +wss://nostrja-kari-nip50.heguro.com +wss://relay.gems.xyz +wss://plnetwork.xyz +wss://nostr.swiss-enigma.sh +wss://geofront.rocks +wss://toot.io +wss://indieweb.social +wss://mastodon.content.town +wss://awaymessage.club +wss://relay.intify.io +wss://mstdn.science +wss://maniakey.com +wss://otadon.com +wss://mstdn.guru +wss://misskey.noellabo.jp +wss://hessen.social +wss://mastodon.top +wss://ipv6.nostr.wirednet.jp +wss://relay.nostr-relay.org +wss://cum.camp +wss://nebbia.fail +wss://current.fyi +wss://bae.st +wss://lolison.network +wss://ioc.exchange +wss://bylines.social +wss://decayable.ink +wss://nostr.unitedserializer.com +wss://urbanists.social +wss://dark-elves.social +wss://writing.exchange +wss://nostr.rikmeijer.nl +wss://misskey.social +wss://ht.nixre.net +wss://mathstodon.xyz +wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.local +wss://abla.news +wss://muenchen.social +wss://homeserver.drake-carp.ts.net:4848 +wss://snailedit.social +wss://mastodon.gamedev.place +wss://tech.lgbt +wss://mast.lat +wss://econtwitter.net +wss://veganism.social +wss://btclolap6mm4tl37huslk6j76enq7qxaj2kwq7w6cdr5ros56eletcqd.onion +wss://toot.cat +wss://nostr.zenon.info +wss://misskey.art +wss://nostr.hushvault.ie:4848 +wss://o6ga6lxnax2z7pgkkenifollohkrv55r36mdzdtofi7d5yyif2f4o5yd.onion:5051 +wss://20nostr-pub.wellorder.net +wss://ecoevo.social +wss://nostr.zerofiat.world +wss://nostr.atitlan.io +wss://detroitriotcity.com +wss://rollenspiel.social +wss://paquita.masto.host +wss://literatur.social +wss://dave.st.germa.in +wss://sunny.garden +wss://nostrpro.xyz +wss://relay.getalby.com +wss://misskey.systems +wss://mamot.fr +wss://social.anoxinon.de +wss://relay.nostr.social +wss://mindmachine.org +wss://microblog.club +wss://relay.utxo.com +wss://universe.nostrich.landlangth +wss://social.teci.world +wss://social.freetalklive.com +wss://mk.absturztau.be +wss://social.ornella.xyz +wss://left-tusk.com +wss://bofh.social +wss://a11y.social +wss://shroomslab.net +wss://relay2.nostr.vet +wss://mugicha.club +wss://laserbeak.local:4848 +wss://annihilation.social +wss://humble.cafe +wss://nostr.relay.damus.io +wss://nostr.milol.lol +wss://nostr.blimpme.app +wss://noc.social +wss://wetdry.world +wss://nostr.taxi +wss://nostr.21-bitcoin.org +wss://relay.llevotu-bitcoiners.info +wss://pl.kitsunemimi.club +wss://djsumdog.com +wss://citadel.local:4848 +wss://federate.blogpocket.com +wss://chaos.social +wss://mastodon.me.uk +wss://oxtr.dev +wss://misskey.cloud +wss://varishangout.net +wss://lacosanostr.com +wss://willem.currycash.net:4848 +wss://lightniningrelay.com +wss://queer.party +wss://lightning.relay.com +wss://mastodon.nl +wss://purplepag.es +wss://cupoftea.social +wss://bitcoiner.socialwss +wss://relay.current.io +wss://sigmoid.social +wss://wandering.shop +wss://braydmedia.de +wss://quey.la +wss://nostr.zebeedee.cloud +wss://nostr-2.zebeedee.cloud +wss://artsio.com +wss://nostr1676031941328.app.runonflux.io +wss://arsip.ddns.net +wss://relay.nostrcitadel.org +wss://relay.nostr-citadel.org +wss://nostr-citadel.org +wss://blastr.f7z.io +wss://nostr.myowndamnnode.com +wss://snowdin.town +wss://nostr.zxcvbn.space +wss://relay.notmandatory.org +wss://bitcoin.social +wss://friendsofdesoto.social +wss://zirk.us +wss://digipres.club +wss://nostr-test.elastos.io +wss://nostr.onsat.org +wss://damus.relay.io +wss://beefyboys.win +wss://nostrija-kari.heguro.com +wss://froth.zone +wss://ns.penseer.com +wss://relay.nostrical.com +wss://nostr.hoshizora.ch +wss://kunigaku.github.io +wss://nostr.shino3.net +wss://umbrel.tailbb128.ts.net:4848 +wss://tailbb128.ts.net:4848 +wss://fedi.twoshortplanks.com +wss://filter.nostr.winebroadcasttrue +wss://nostr.doufu-tech.com +wss://relay.hodl.haus +wss://pgh.social +wss://coeditor-congested.fractalnetworks.co +wss://mastodon.podaboutli.st +wss://frenfiverse.net +wss://nostr.f4255529.fun +wss://nostream.megadope.snowinning +wss://kiritan.work +wss://forever21.lol +wss://zitron.net +wss://mastodon-swiss.org +wss://relay1.current.fyi +wss://nostr.plebs.com +wss://multiplextr.corocal.social +wss://nostr.zedebee.cloud +wss://nostr.mutinywallet.comisntbanned.addthatonesotheycangetnotesrelayedtotherestofthenetworkfrominsideofchina +wss://mastodon.mit.edu +wss://relaynostrati.com +wss://relaynostr.band +wss://relaynostr.info +wss://relaychenxixian.cn +wss://relaysnort.social +wss://relaynostr.com.au +wss://nostr-tbd.website +wss://relay.hoshizora.ch +wss://social.balsillie.net +wss://strangeobject.space +wss://relay.nvote20.co +wss://fla.red +wss://urusai.social +wss://chad.polytechnic.com +wss://robo358.com +wss://conxole.io +wss://relay.ohbe.me +wss://nostr.bitcoiner.com +wss://www.mutinywallet.com +wss://x.9600.link:10070 +wss://a11y.info +wss://homelab.host +wss://k65qz57zx4sw24fow2bvchjgmpjqljj4cp3oa7dtpbbprjifsdotggid.local +wss://relay.vtuber.directory +wss://x.9600.link:8000 +wss://nostr.plebs.win +wss://renkontu.com +wss://nost.massmux.com +wss://submarin.online +wss://social.librem.one +wss://relai.kongerik.et +wss://mstdn.io +wss://astral.swiss-enigma.ch +wss://stereophonic.space +wss://relayer.pleb.social +wss://mastodon.nz +wss://nostr.nostr.de +wss://nostr.thezap.club +wss://proxy.shroomslab.net +wss://pfr24mrpxowclhm4y6adu36kbo3erx7gskzyfqqhnhfpdkdmylpfgkyd.local +wss://nostr.hodl.haus +wss://vtdon.com +wss://relay.nost.band +wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.onion +wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.local +wss://md.hugo.nostr +wss://boks.moe +wss://truthsocial.co.in +wss://mastodon.xyz +wss://relay.universalname.space +wss://relay-nostr.wirednet.jp +wss://fedi.pawlicker.com +wss://favcalc.com +wss://rot13maxi.com +wss://awayuki.net +wss://mazinkhoury.com +wss://g0v.social +wss://shpposter.club +wss://a.lufimianet.jp +wss://pura20vida.nostr.land +wss://sotalive.net +wss://sendsats.lol +wss://vida.page +wss://wagvwfrdrikrqzp7h3b5lwl6btyuttu7mqpeji35ljzq36ovzgjhsfqd.onion +wss://uec2cmjauzufrtlq6wq6l2ujfncdvo3suezz423gsvz5xvhehm2mcgid.onion +wss://mastodon-belgium.be +wss://rejecttheframe.xyz +wss://nogood.store +wss://getaiby.com +wss://plebchain.club +wss://nostrverified.com +wss://x.9600.link +wss://n0p0.shroomslab.net +wss://orangemakura.xyz +wss://jamw.net +wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.local +wss://snabelen.no +wss://floss.social +wss://mastoot.fr +wss://kmc-nostr.amiunderwater.com +wss://hodl.camp +wss://xmr.usenostr.com +wss://nostr-pub.wellorn.net +wss://gudako.net +wss://ryona.agency +wss://kafeneio.social +wss://massmux.com +wss://freezepeach.online +wss://nostream-production-f83d.up.railway.app +wss://taobox.pub +wss://mastodon.radio +wss://einundzwanzig.relay.com +wss://journa.host +wss://social.here.blue +wss://toot.wales +wss://staging.nostr.com.se +wss://pleroma.soykaf.com +wss://berserker.town +wss://zapforart.site +wss://forall.social +wss://nostrja-kari.heguro.comyee +wss://higheredweb.social +wss://social.gnuhacker.org +wss://the.hodl.haus +wss://ng4jk6yiqgfczo4wyxszuj7w6jok3fptehu533o3mlzs3vph3dvjfdid.onion +wss://nostr.mtpx.ovh +wss://relay.coollamer.com +wss://glasgow.social +wss://fedi.absturztau.be +wss://nostr.frostr.xyz +wss://relay.runningnostr.lol +wss://relay2.vtuber.directory +wss://nerdculture.de +wss://nostr-relay.untethr.meoperator +wss://nostr.verif-slothy.win +wss://nostr.pub +wss://social.process-one.net +wss://invillage-outvillage.com +wss://otofu.uk +wss://universe.nostrich.landlangenlangpt +wss://mastodon.iriseden.eu +wss://frighteningdeafeningagent.nailuogg.repl.co +wss://obo.sh +wss://relay.hodlhaus.net +wss://rdrama.cc +wss://nostr.packetlostandfound.us +wss://test.relay +wss://mastodonbooks.net +wss://southflorida.ninja +wss://blob.cat +wss://tooting.ch +wss://relay.pineapple.pizza +wss://relay.nostr.directory +wss://relar.nostr.bg +wss://reisen.church +wss://relay.honk.pub +wss://nostr.rsfriedl.com +wss://relay.nort.social +wss://1611.social +wss://the.hodl.house +wss://relayable.com +wss://fedi.syspxl.xyz +wss://nostrpub.yeghro.site +wss://skr5bbrgzfnideglw4cs2iw6au2jm2b7gupocxbmkv5qopo6rcitmiqd.local +wss://a2mi.social +wss://status.relayable.org +wss://toot.blue +wss://btcqspp5dl4rlgl5pomcyv3odfeki7a5zrmjoekyu5vsoqz5bth4e7yd.onion +wss://bitcoinr6de5lkvx4tpwdmzrdfdpla5sya2afwpcabjup2xpi5dulbad.onion +wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.onion +wss://dnze4ekho2kuiejwatjw5omeprtmdaum2ukok52roiu5rztii3rp2aid.onion +wss://53snncs7vegargpaardbxjnii2oan3xpmbeaf6czwoqa2axz5mvbsjid.onion +wss://fnqdhz3df33da6wxg7jskvumd5rjn3nknln6ecun7uwwysc7vkwkjgid.onion +wss://relay.thefockinfury.wtf +wss://silliness.observer +wss://freak.university +wss://piaille.fr +wss://nostr.weking.tk +wss://f.reun.de +wss://radixrat.com +wss://hostux.social +wss://chat.freenode.net +wss://no.str..cr +wss://nostr.inoata.cc +wss://kappa.seijin.jp +wss://floyds.io +wss://mast.dragon-fly.club +wss://zbd.ai +wss://misc.name +wss://pdx.social +wss://0w0.is +wss://d6jvu2tev2rblkuzgu4ydw2413jizr53j26ut47hxpykvtusbvekhiid.onion +wss://d6jvu2tcv2rblkuzgu4ydw24l3jizr53j26ut47hxpykvtusbvekhiid.onion +wss://drcassone.social +wss://mastodon.energy +wss://rayci.st +wss://ischool.social +wss://nostr.halfway2forever.com +wss://relay.nostr.rocks +wss://nostr.stoner.com +wss://2nodez.com +wss://rs.nostr-x.com +wss://schleuss.online +wss://thrashzone.org +wss://relay.nostrgraph.com +wss://relay.2nodez.com +wss://occult-zuki.com +wss://mastodon.lithium03.info +wss://bigbadpc.local:4848 +wss://social.gr0k.net +wss://nostr.wirednet.jp +wss://relay.plebster.com +wss://bitcoin.nostr +wss://mastodon.im +wss://brb..io +wss://nostr.sandwhich.farm +wss://relay.nos.lol +wss://beta.nostr.v0l.io +wss://eden.nostr.space +wss://powerlay.xyz +wss://rap.social +wss://relay.mutinywallet.com +wss://rogue.earth +wss://600.wtf +wss://klabo.blog +wss://petrikajander.com +wss://tgkzmdd.help +wss://nostr.red +wss://brb.lol +wss://jz2l2bf6f6wssdqwkg7ogthkc5i3ymyiwkaz3tbhff6ro3h3zqddekyd.onion +wss://mstdn.mini4wd-engineer.com +wss://nostr.exposd +wss://nostr.a-ef.org +wss://computerfairi.es +wss://social.targaryen.house +wss://o3o.ca +wss://walkah.social +wss://cosocial.ca +wss://filter.nostr.band +wss://relais.nostrview.com +wss://gigaohm.bio +wss://dobbs.town +wss://bark.lgbt +wss://mastodon.gal +wss://snug.moe +wss://genomic.social +wss://relay.orangepilldev.com +wss://social.sdf.org +wss://social.camph.net +wss://mstdn.poyo.me +wss://nein.lol +wss://nostr.i00.org +wss://kemono.ink +wss://mu.zaitcev.nu +wss://libera.site +wss://ca.hibi-tsumo.com +wss://social.bund.de +wss://xscape.top +wss://social.lol +wss://birds.town +wss://arnostr.com +wss://nostr.33co.de +wss://relay.nostr.lighting +wss://metadata-contacts-relays.pages.dev +wss://webs.node9.org +wss://pleroma.elementality.org +wss://suya.place +wss://livellosegreto.it +wss://peoplemaking.games +wss://nattois.life +wss://typo.social +wss://neutrine.com +wss://ragner-relay.com +wss://wss.nostr.uselessshit.co +wss://wss.nostrue.com +wss://relay.nvote.co:433 +wss://disobey.net +wss://rneetup.com +wss://arnostr.com:8433 +wss://relay.uxto.one +wss://relay.hackerman.pro +wss://thisis.mylegendary.quest +wss://poliversity.it +wss://sats.lnaddy.com +wss://rs2.abaiba.top +wss://rs1.abaiba.top +wss://rs2.abaiba.top.abaiba.top +wss://social.matarillo.com +wss://nostr01.counterclockwise.io +wss://backup.local:4848 +wss://touhou.vodka +wss://mi-wo.site +wss://nostr.f7z.io +wss://alive.bar +wss://strfry.nostr-x.com +wss://mastodontti.fi +wss://nostr.wellorder.net +wss://y.9600.link:8000 +wss://ephemrelay.mostr.pub +wss://byc-italia.online +wss://fissionator.com +wss://stranger.social +wss://eupolicy.social +wss://nostr-desktop.local:4848 +wss://aoir.social +wss://mstdn.plus +wss://nostrproxy.io:3333 +wss://mastodon.hams.social +wss://jorts.horse +wss://metalhead.club +wss://dice.camp +wss://mstdn.y-zu.org +wss://loffchain.pub +wss://mastodon.llarian.net +wss://nostr-relay2.thefockinfury.wtf +wss://2g2jzcfgq5lcrceuq23lmya2drm3ku5qmqimr3bvu3amol55vidctrad.onion +wss://mastodon.au +wss://bgme.me +wss://nostr.badran.xyz +wss://nostr.coincreek.com +wss://nostream-test.up.railway.app +wss://relay.blackthunder.click +wss://relay.grorp.com +wss://atomicpoet.org +wss://iddqd.social +wss://gusto.masto.host +wss://lifehack.social +wss://blorbo.social +wss://freecumextremist.com +wss://13bells.com +wss://rsslay.nostr.netrelay +wss://relay.zerosequioso.com +wss://nauka-relay.herokuapp.com +wss://nostr.nofdeofsven.com +wss://out.of.milk +wss://cawfee.club +wss://r.relay.fan +wss://alo.ottonove891.cf +wss://keinoha.tailnet-0240.ts.net +wss://nostr.paralelnipolis.cz +wss://tuiter.rocks +wss://elizur.me +wss://nostr-dev.newstr.io +wss://discuss.systems +wss://blahaj.zone +wss://mastodon.art +wss://makersocial.online +wss://gamepad.club +wss://nostr.flameofsoul.ru +wss://dmv.community +wss://relay.nostr.com +wss://nostr-desktop.saiga-shark.ts.net:4848 +wss://nostrfoxden.ddns.net:4848 +wss://soc.umrath.net +wss://ravenation.club +wss://oisaur.com +wss://nostr-relay.net +wss://social.mikutter.hachune.net +wss://lewacki.space +wss://fediscience.org +wss://todon.eu +wss://nuccy-nuc7i5bnk.local:4848 +wss://games.gamertron.net:4848 +wss://fiedlerfamily.net +wss://postnstuffds.lol +wss://nostr.planetary.social +wss://worldkey.io +wss://hcommons.social +wss://gymp7qquljs47xbbvs47hkptnyyzegy2jkst26mkjxaciifqffjatqid.onion +wss://rs3.abaiba.top +wss://sudo-nostr.com +wss://satgag.site +wss://nostr.lnbitcoin.cz +wss://relay20nostrplebs.com +wss://2pbkpndvpeebljfvjew6auq63lndzszqnntct5aqfmazslerzxe75kad.onion +wss://dragonchat.org +wss://welcome.nostr.wine +wss://relay.nostr.land +wss://social.linux.pizza +wss://dnppj4kopczovvzvpzmihv2iwe5wt3gbrxjnltjc2zdjpttrdz4owpad.onion +wss://potofu.me +wss://nostrbr.online +wss://mastorol.es +wss://notebook.taild34d0.ts.net +wss://t.aqn.jp +wss://nostr.mutinywallet +wss://wcone.nostr.wine +wss://norden.social +wss://eostagram.com +wss://shigusegubu.club +wss://toot.jkiviluoto.fi +wss://kiwifarms.cc +wss://swiss-talk.net +wss://v532btfg2fb4za2g476a7w23pgpkllc7uq274wqtktwjogt5ynb3ukqd.local +wss://mstdn.maud.io +wss://arc1.arcadelabs.com +wss://nostr.jp +wss://relay-jp.nostr.wirrdnet.jp +wss://climatejustice.social +wss://witter.cz +wss://mastodon.pnpde.social +wss://ttrpg-hangout.social +wss://beehaw.org +wss://thecanadian.social +wss://nostr.fbxl.net +wss://relay.sandwich.farm +wss://nostr.olwe.link +wss://botsin.space +wss://zeroes.ca +wss://photog.social +wss://paid.nostr.lc +wss://free.nostr.lc +wss://test.nostr.lc +wss://gzanlkgurj7zd3psqms3da4vrw4imurnyyzaycfuiiug7elqow7xlayd.onion:5051 +wss://masto.nu +wss://mastodon.uy +wss://bit.relay.center +wss://offchain.relay.center +wss://damus.relay.center +wss://wine.relay.center +wss://eden.relay.center +wss://moth.social +wss://nostr.masmux.com +wss://chrome.pl +wss://mastodon.ktachibana.party +wss://ak.kawen.space +wss://mementomori.social +wss://relay.s3x.social +wss://lnbits.michaelantonfischer.com +wss://yof23ggqmert72c5wcl5qglphapy3o2xjdedtkbrn2dt5rbae2s7f6qd.onion +wss://relay.snort.socail +wss://post.lurk.org +wss://yiff.life +wss://q3zaylwjjhq77yzx34lbydz26szzjljberwetkjgxgsapcekrpjzsmqd.onion +wss://lnbits.b1tco1n.org +wss://welcome.nostr.relay +wss://sound-money-relay.denizenid.com +wss://carnivore-diet-relay.denizenid.com +wss://africa.nostr.joburg +wss://nostr.jolt.run +wss://nostr.chainbits.co.uk +wss://ithurtswhenip.ee +wss://nostr.cloudversia.com +wss://relay1.east.us.nostr.btron.io +wss://ca.orangepill.dev +wss://pdx.land +wss://linh.social +wss://okla.social +wss://androiddev.social +wss://spore.social +wss://mastodo.fi +wss://kabedon.space +wss://nost.inosta.cc +wss://relay2cdamus.io +wss://nostr.openhoofd.nl +wss://dragonscave.space +wss://genart.social +wss://dewp.space +wss://layer8.space +wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion:5051 +wss://nostr.wines +wss://relay.snort.relay.ryzizub.com +wss://pixelfed.de +wss://nostr.holyscapegoat.com +wss://nostr.einunzwanzig.space +wss://nostr.hifish.org +wss://colearn.social +wss://topspicy.social +wss://mastodon.neat.computer +wss://relay.nostr.hach.re +wss://nostr.dakukitsune.ca +wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.onion +wss://esq.social +wss://famichiki.jp +wss://tribe.net +wss://masto.nobigtech.es +wss://umbrel-nuc.local:4848 +wss://mastodon.cocoasamurai.social +wss://debian.taildd32b.ts.net:4848 +wss://vlt.ge +wss://relay.johnnyasantos.com +wss://snort.relay.center +wss://nb.relay.center +wss://waag.social +wss://concentrical.com +wss://stat.rocks +wss://oransns.com +wss://relay-jpp.nostr.wirednet.jp +wss://oc.todon.fr +wss://jundow.gitlab.io +wss://neurodifferent.me +wss://jazztodon.com +wss://nostrich.friendship +wss://indieauthors.social +wss://werunbtc.com +wss://frogtalk.lol +wss://pop-os.local:4848 +wss://fediver.de +wss://d6qxo55dhms6revgrmbindvb5ejd3gw5hrji7ylkm6khghii3hjs3uyd.onion +wss://eldritch.cafe +wss://karlsruhe-social.de +wss://social.yl.ms +wss://nostr.mycloudhouse.duckdns.org +wss://nostr.otc.sh +wss://nya.social +wss://relay2.nostrchat.io +wss://relay1.nostrchat.io +wss://nostrja-world-relays-test.heguro.com +wss://ndk-relay.local +wss://reespeech.casa +wss://pleroma.atyh.cc +wss://lawfedi.blue +wss://akkoma.jasminetea.uk +wss://peeledoffmy.skin +wss://plush.city +wss://astrodon.social +wss://samenet.social +wss://toot.bike +wss://mi.yukioke.com +wss://social.growyourown.services +wss://mastodon.nu +wss://lou.lt +wss://functional.cafe +wss://relaydamus.io +wss://coma.social +wss://social.fbxl.net +wss://biplus.social +wss://toots.matapacos.dog +wss://psychoet.ml:3250 +wss://danserver.equipment +wss://nostr.lacrypta.com.ar +wss://wonkodon.com +wss://nostr.seankibler.com +wss://autistics.life +wss://cambrian.social +wss://rvqkqr5kl3dvvxyn67rfowcnvoflx4zby5tjbysavym4ycckti4dbjyd.onion +wss://swiss.nostr.lc +wss://snac.saifulh.online +wss://loma.ml +wss://nostr.privoxy.io +wss://366.koyomi.online +wss://mastodon.stormy178.com +wss://podcastindex.social +wss://bitcoiner.socia +wss://fedi.ml +wss://replayable.org +wss://nostr.schroomslab.net +wss://nostr.global.fans +wss://relay.weedstr.net +wss://vocalodon.net +wss://relay.nostr.bandadd +wss://nostr.oxtr.devadd +wss://jarvis.taild68e2.ts.net:4848 +wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.onion +wss://stonez.local:4848 +wss://notrustverify.ch +wss://woof.group +wss://mastodon.floe.earth +wss://lnbits.plebtag.com +wss://kpop.social +wss://relay.wavlake.com +wss://mastodon.sharma.io +wss://travelpandas.fr +wss://alphapanda.prowss +wss://relay.saes.io +wss://barelysocial.org +wss://masto.komintern.work +wss://norcal.social +wss://nostr.zbd.gg +wss://mk.outv.im +wss://mstdn.mx +wss://col.social +wss://nostr.freedom.fi +wss://filter.nostr.winebroadcasttrueglobalall +wss://eden.nostr.landv +wss://me.dm +wss://emacs.ch +wss://winonostr.wine +wss://infoplebstr.com +wss://mas.towss +wss://gruene.social +wss://relay.freeplace.nl +wss://itis.to +wss://bsky.social +wss://lnbits.thefockinfury.wtf +wss://5xxkt7zvmh4zdsjw64lgvchjdlrrgw4w2huujiiud35qms6gnkn5azad.onion +wss://eientei.org +wss://artisan.chat +wss://nustr.mom +wss://relay.nostrhraph.net +wss://shitposter.club +wss://nostril.cam +wss://nostr.spaceshell.xyz +wss://relay.wtr.app +wss://tdd.social +wss://d3meec25b53kegrnjmtmtyynikbkmuxf4jqgtk3sonjs6e62hpaezyqd.onion +wss://tkz.one +wss://freerelay.xyz +wss://nfdn.betanet.dotalgo.io +wss://mitra.social +wss://hablanews.io +wss://calle.wtf +wss://voskey.icalo.net +wss://nostr.sloyhy.win +wss://framapiaf.org +wss://nostrnodeofsven.com +wss://ciberlandia.pt +wss://gnusocial.net +wss://relay.s3x.socia +wss://paste.2nodez.com +wss://union.place +wss://bofh.socia +wss://sovbit.dev +wss://lnbits.btc-payserver.eu +wss://osna.social +wss://im-in.space +wss://junxingwang.org +wss://relay.nostr.wirednet.jpcheck +wss://libranet.de +wss://fedisnap.com +wss://woodpecker.social +wss://gensokyo.town +wss://social.imirhil.fr +wss://links.potsda.mn +wss://law-and-politics.online +wss://nostrich.bar +wss://tweesecake.social +wss://nostr.kisiel.net.pl +wss://relay.kisiel.net.pl +wss://calckey.social +wss://nostr.cercatrowa.me +wss://meganekeesu.tokyo +wss://lndiscs.duckdns.org +wss://omochi.xyz +wss://wue.social +wss://nostr.libreleaf.com +wss://rusnak.io +wss://rsslay-production.up.railway.app +wss://nostr.yuhr.org +wss://bod4ojj37fneith2setv3qjbii563wesbjqgdipdz4ag6voic2xk5iad.onion +wss://fediverse.blog +wss://pouet.chapril.org +wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.local +wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.onion +wss://cryptodon.lol +wss://nostr.debancariser.com +wss://mizunashi.hostdon.ne.jp +wss://relay.deezy.io +wss://bbq.snoot.com +wss://historians.social +wss://mi.mashiro.site +wss://mastodonpost.social +wss://nostrpub.welliorder.net +wss://social.cologne +wss://metapixl.com +wss://wandzeitung.xyz +wss://lightninhrelay.com +wss://techopolis.social +wss://lnbits.btcpins.com +wss://purplenostrich.com +wss://onewilshire.la +wss://sself.co +wss://anygemini13.blogs.sapo.pt +wss://federated.press +wss://metadata.nostr.com +wss://nostr.hodl.ar +wss://goreslut.xyz +wss://mastodon.com.tr +wss://climatejustice.global +wss://brotka.st +wss://sueden.social +wss://mstdn.fr +wss://abid.cc +wss://lnbits.fuckedbitcoin.com +wss://meow.social +wss://nostr.rehab +wss://mstdn.media +wss://nodeo1.nostress.cc +wss://nostrmassmux.com +wss://sauropods.win +wss://civilians.social +wss://pnw.zone +wss://zebeedee.cloud +wss://nostr.walletofsatishi.com +wss://aufovmqaxj5nhqmtorhgpogdjxefhkff25cbyyjt2sub3vwg6b6rplid.onion +wss://forfuture.social +wss://76f67qcwxsxpz7cfozlzunota2ejqznpldc5pnqtyq233hjpjzrmlfid.local +wss://toot.garden +wss://umbrel.tail9dfb.ts.net:4848 +wss://mastodon.chasem.dev +wss://misskey.pm +wss://merovingian.club +wss://chirp.enworld.org +wss://paid.no.str.ce +wss://masto.bike +wss://masto.1146.nohost.me +wss://eosla.comno-str.orgrelay.zeh.appno-str.orgrelay.zeh.app +wss://nixnet.social +wss://pay.zapit.live +wss://respublicae.eu +wss://nekomiya.net +wss://mastodon.internet-czas-dzialac.pl +wss://plushies.social +wss://lnb.openchain.fr +wss://mastodon.lol +wss://social.rebellion.global +wss://ruhr.social +wss://mi.farland.world +wss://pkutalk.com +wss://systemli.social +wss://nostr.minimue81.selfhost.com +wss://mastodon.la +wss://everything.happens.horse +wss://pagan.plus +wss://clacks.link +wss://u-tokyo.social +wss://fediverse.projectftm.com +wss://mastodon.bachgau.social +wss://social.kabi.tk +wss://ty3zdjkwlxo4zah6tgdoolznjcbvkhxpcvjyqe2buxeg23hbeyvr3rad.local +wss://pleroma.wakuwakup.net +wss://social.horrorhub.club +wss://nostr.nightowlstudios.ca +wss://mastodon.ml +wss://relay.orangepillapp.com +wss://misskey.sup39.dev +wss://mfmf.club +wss://pokemon.mastportal.info +wss://gohan-oisii.net +wss://aipi.social +wss://nostr.semisol.com +wss://oslo.town +wss://relay.layer.systems +wss://naharia.net +wss://social.elbespace.de +wss://linuxrocks.online +wss://b81m3pf94ridtry53g8ufyyrjtjaoxgbyjbs5k8qrqkr1whocxiy.loki:8080 +wss://lvl01.tater.ninja +wss://kinky.business +wss://relay.bitblockboom.com +wss://fedi.omada.cafe +wss://social.secret-wg.org +wss://celebrity.social +wss://weirdo.network +wss://mastodon.design +wss://berlin.social +wss://misskey.yukineko.me +wss://mindmachine.688.org +wss://sackheads.social +wss://a.farook.org +wss://social.ridetrans.it +wss://nostr-2.crypticthreadz.com +wss://test.itas.li +wss://fashionsocial.host +wss://ordinary.cafe +wss://social.arinbasu.online +wss://nostr.crypticthreadz.com +wss://relay.zebedee.cloud +wss://nostr.pub.wellorder.net +wss://09d4-5-161-189-144.ngrok-free.app +wss://andalucia.social +wss://udongein.xyz +wss://squeet.me +wss://mastodon.org.uk +wss://guild.pmdcollab.org +wss://relay.fi +wss://black.nostrscity.club +wss://relay.darker.to +wss://denostr.paiya.app +wss://noste.lu.ke +wss://wss.node01.nostress.cc +wss://theverge.space +wss://swiss.social +wss://relay.webstr.org +wss://nostr.shsbt.xyz +wss://relay.nostr.mom +wss://bitcoinmaximlaists.online +wss://relay.openhoofd.nl +wss://toot.aquilenet.fr +wss://toot.ale.gd +wss://relay.devstr.org +wss://lounge.town +wss://amala.schwartzwelt.xyz +wss://planetasieve.com.br +wss://alcrypt.ru:20911 +wss://webzero.grin.plus:8080 +wss://pylons.lightlns.com:28556 +wss://etourneau.fr:28343 +wss://sentie.relay.rts.network +wss://hermes.boarstudios.com +wss://non-central.pw +wss://nostrum.casa +wss://press.coop +wss://neovibe.app +wss://mstdn.starnix.network +wss://nostr.ameristraliagov.com +wss://cryptodon.chat +wss://umbrell.local:4848 +wss://mastodon.codingfield.com +wss://fe.disroot.org +wss://national.catposting.agency +wss://mastodon.pinewoodroad.net +wss://podcasts.social +wss://20nostr.semisol.dev +wss://nostr.oxtr.net +wss://mstdn.o-nature-culture.net +wss://dearcoati6.lnbits.com +wss://die-partei.social +wss://donotban.com +wss://creative.ai +wss://metaskey.net +wss://spacey.space +wss://node01.nostreess.cc +wss://node01.nostress.co +wss://niscii.xyz +wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.onion +wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.local +wss://sportsbots.xyz +wss://videos.lukesmith.xyz +wss://nostr.btcfreedom.ca +wss://gardenstate.social +wss://bg-btc.local:4848 +wss://0fa53e299287.ngrok.app +wss://bird.makeup +wss://nlayer.lbdev.fun +wss://relay.queiroz.vip +wss://mstdn.business +wss://osage.moe +wss://botrelay.com +wss://filter.wine +wss://gingadon.com +wss://noncentral.pw +wss://honi.club +wss://xn--baw-joa.social +wss://nostr.semisol.devwss +wss://relay.iris.to +wss://mynostrrelay.deno.dev +wss://social.heise.de +wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5051 +wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5050 +wss://relao.nostr.bg +wss://phpc.social +wss://mastodon.kylerank.in +wss://gameliberty.club +wss://rot.gives +wss://www.nostrweb.xyz +wss://ligma.pro +wss://mastodon.grin.hu +wss://geeknews.chat +wss://devdilettante.com +wss://relay.nosr-latam.link +wss://nosr.bitcoiner.social +wss://raru.re +wss://create-key.net +wss://lgbtqia.space +wss://fluffy.family +wss://mas.town +wss://bird.froth.zone +wss://akkoma.cryptoschizo.club +wss://relay.ramus.io +wss://40two.site +wss://relay.40two.site +wss://vis.social +wss://mk.paritybit.ca +wss://nyan.network +wss://ln.weedstr.net +wss://wikis.world +wss://social.fringe.com +wss://umbraxenu.no-ip.biz +wss://heads.social +wss://tsqdakwo4dh5ej3llsi52ftxfbialteu3jm4cmvxaksl3psbyeoyxxqd.onion +wss://jameliris.to +wss://mastodon.thirring.org +wss://miniwa.moe +wss://welcom.nostr.wine +wss://nostr.vulpem.comwss +wss://relay.semisol.dev +wss://kitsunes.club +wss://songbird.cloud +wss://nostr.wyssblitz.org +wss://libera.tokyo +wss://trpger.us +wss://comam.es +wss://nostr-pub.semisol.devaddittoyourrel +wss://social.dev-wiki.de +wss://ostfrie.se +wss://darmstadt.social +wss://nostr.cx.ms +wss://alentours.cc +wss://nostr.kleofash.eu +wss://gib.social +wss://test23.hifish.org +wss://rapemeat.solutions +wss://filter.stealth.winebroadcasttrue +wss://nostr.montre +wss://grumble.social +wss://nostr.roli.social +wss://primarycare.app +wss://hodlr.rocks +wss://superlinks.me +wss://mastodon.lawprofs.org +wss://tictoc.social +wss://nostest.dojotunnel.online +wss://kafka.icu +wss://nostr.lanparty.one +wss://filter.nostr.wineglobaltrue +wss://social.b10m.net +wss://worm.pink +wss://nostr-test.cx.ms +wss://nostrverifired.com +wss://relay.nostrss.re +wss://eliitin-some.fi +wss://dju.social +wss://jeremy.hu +wss://stream.criminallycute.fi +wss://nostr.0x50.dev +wss://n.s.nyc +wss://n.8.s.nyc +wss://relay.rocks +wss://relay.fiatjaf.com +wss://n-lan.s.nyc +wss://sciences.social +wss://nostr.swiss-enigma.com +wss://quietplace.xyz +wss://universe.nostrich.landlangenlangzh +wss://5280.city +wss://feddit.de +wss://base.lc +wss://social.medusmedia.com +wss://umha4zl6xk62a4dous6e7tq4qlmt462hlzs2su33en6qrvtvs3hkjgid.onion +wss://etorneau.fr:28343 +wss://foggyminds.com +wss://neurodiversity-in.au +wss://nostr.hendrixson.net +wss://ramen-fsm.eu.org +wss://www.nostrical.com +wss://feedbeat.me +wss://relay.bsky.social +wss://babka.social +wss://mastodontech.de +wss://commiespace.duckdns.org +wss://openbiblio.social +wss://karkatdyinginagluetrap.com +wss://relay.fundr.vanderwarker.family +wss://hannover.town +wss://nostr.relay.info +wss://mastodon.tetaneutral.net +wss://cache2.primal.net +wss://nostr.petrkr.net +wss://ifwo.eu +wss://mastodong.lol +wss://venera.social +wss://wallets.fyoumoneypod.com +wss://nostr.relay-nokotaro.com +wss://social.exozy.me +wss://gqgjp2bun4opme6mepz3rrgprkw4xatb6h5ogayorqwi6sajsxcp5sad.local +wss://branle.netlify.app +wss://rsslay.fiat.jaf +wss://chrislace.damus.io +wss://relay.leafbodhi.com +wss://social.opendesktop.org +wss://relay.nostr.watch +wss://gearlandia.haus +wss://freiburg.social +wss://atlas.nostro.land +wss://nostr.io +wss://patrizio.tn.al +wss://chaintools.io +wss://kn.icu +wss://relaywithme.eu +wss://la-autopilot-this-end-up.dvm.email +wss://coinfinity.co +wss://assemblag.es +wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion +wss://indigenouscreatives.social +wss://bookwyrm.social +wss://kokoro.shugetsu.space +wss://eden.nost.land +wss://misskey.gothloli.club +wss://sersleepy.com +wss://rsslay.nos.pink +wss://pettingzoo.co +wss://witches.live +wss://7craxnzfi42touzi23etut5qjzqro27sqcuottxj7opntcin4fstruad.onion +wss://mastodonsweden.se +wss://mv2k.com +wss://nostr.tchaicap.space +wss://relay.whoop.ph +wss://bitcoiner.nostr.social +wss://kinkyelephant.com +wss://www.superstork.org +wss://mastodon.iftas.org +wss://lea.pet +wss://zug.network +wss://homeserver.local:4848 +wss://frontrange.co +wss://sciencemastodon.com +wss://montereybay.social +wss://social.securecryptomining.com +wss://rssrelay.nostr.moe +wss://nostr.org +wss://nostr.tw +wss://nostr.hk +wss://wxw.moe +wss://mastodonmusic.social +wss://puntarella.party +wss://oyasumi.space +wss://drumstodon.net +wss://social.wikimedia.de +wss://iyasaretai.pw +wss://social.coletivos.org +wss://nostream.localtest.me +wss://ubuntu201.local:4848 +wss://masto.pt +wss://taiwan.riley-tech.net +wss://freeradical.zone +wss://blastrf7z.xyz +wss://onemorestop.photo +wss://frikiverse.zone +wss://toot.bldrweb.org +wss://electroverse.tech +wss://mstdn.games +wss://relay1.nostr.unitedfop.com +wss://gratefuldread.masto.host +wss://mk.gabe.rocks +wss://widerweb.org +wss://mastodon.eternalaugust.com +wss://lay.southeastasia.cloudapp.azure.com:445 +wss://me.ns.ci +wss://gnostr.th +wss://fritter.cn +wss://nex.cn +wss://nex.tw +wss://fritter.jp +wss://fritter.tw +wss://gnostr.cn +wss://mstdn.dk +wss://akkoma.simulacrum-emporium.eu +wss://dalliance.social +wss://toot.re +wss://avatastic.uk +wss://blastr20f7z.xyz +wss://the.voiceover.bar +wss://porcodon.net +wss://nostr.dncn.xyz +wss://nostr.dnxn.xyz +wss://relay.xmr.rocks +wss://d6egak3woofrixu26gr3utb5qezhkktavsuwlrfqaauu55lmpudxudqd.onion:5051 +wss://social.wuebbsy.com +wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.local +wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.onion +wss://tooters.org +wss://nostre.wine +wss://relay.xplive.local +wss://relay2.xplive.local +wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.onion +wss://pipou.academy +wss://opjk6jxrcyicuwhe62tqy6zwx776u7rfi6cqo6iodurjvege7piz5wqd.local +wss://mythology.social +wss://nostr.millou.lol +wss://ryogrid.net:7777 +wss://nost.debancariser.com +wss://fault.stsecurity.moe +wss://jan-optiplex-5040.local:4848 +wss://relay-jp.wirednet.jp +wss://mastodon.kitchen +wss://relay.mnethome.de +wss://verkehrswende.social +wss://nostr.gleeze.com +wss://dair-community.social +wss://shota.house +wss://kavlak.uk +wss://social.inex.rocks +wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.local +wss://startrekshitposting.com +wss://nostrfmar.ddns.net +wss://4yqp7gzuf15zfc3hpwhz3j5p2uarvdsnf75ovpdiqvyjdsmku771jfid.onion +wss://nostrgraph.net +wss://idolheaven.org +wss://mstdn.kemono-friends.info +wss://mastodon.bawue.social +wss://social.pmj.rocks +wss://ursal.zone +wss://nstr.milou.lol +wss://poweredbygay.social +wss://gochisou.photo +wss://lnb3.openchain.fr +wss://nostr.filmweb.pl +wss://nostr-word.h3z.jp +wss://blastr.f7z.xyzanotherinstanceofblastr +wss://shakedown.social +wss://nostr.bubu.hair +wss://hyper-nostr.inosta.cc +wss://ieji.de +wss://wawmartme.com +wss://nostr.kungfu-g.rip +wss://bozgor.org +wss://todon.nl +wss://nostpy.lol +wss://ostatus.taiyolab.com +wss://polsum.rocks +wss://freespeech.group +wss://im.allmendenetz.de +wss://shitpost.poridge.club +wss://twingyeo.kr +wss://social.platypush.tech +wss://rsslay-production-bc22.up.railway.app +wss://lonely.damus.io +wss://kirche.social +wss://cubalibre.social +wss://relay.txinito.xyz +wss://realy.orangepill.dev +wss://3zi.ru +wss://plebstr.com +wss://social.seattle.wa.us +wss://social.bim.land +wss://cubhub.social +wss://relay.nostr-x.com +wss://hispagatos.space +wss://node101.nostress.cc +wss://lsbt.me +wss://jgqaglhautb4k6e6i2g34jakxiemqp6z4wynlirltuukgkft2xuglmqd.onion +wss://nostdemo.dojotunnel.online +wss://f.cz \ No newline at end of file diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/SCrypt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/SCrypt.kt index d49660aed..14a3fe884 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/SCrypt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/SCrypt.kt @@ -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) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt new file mode 100644 index 000000000..0d526ab17 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt @@ -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") + } +} diff --git a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt new file mode 100644 index 000000000..4a6ccb8f9 --- /dev/null +++ b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt @@ -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(), + ) + } +} diff --git a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt new file mode 100644 index 000000000..5feaa1621 --- /dev/null +++ b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt @@ -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) + } +} diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.ios.kt index 05bc3456a..ab7092849 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.ios.kt @@ -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>, + 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>, 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>, 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>, 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) } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfoParser.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfoParser.ios.kt index f65a70dd9..4e6a97a54 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfoParser.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip96FileStorage/info/ServerInfoParser.ios.kt @@ -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 + } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt index ac15ab144..51754a49e 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt @@ -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) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt index f68ed6eff..c782de2f9 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt @@ -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() } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.ios.kt index fe21a7f10..771c35916 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.ios.kt @@ -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) } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt index 89e974fca..ad697c7f9 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt @@ -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 { - TODO("Not yet implemented") + val detectorInstance = UrlDetector() + return detectorInstance.findURLsWithText(text) as List } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UnicodeNormalizer.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UnicodeNormalizer.ios.kt index 6a44f4c5c..56971b710 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UnicodeNormalizer.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UnicodeNormalizer.ios.kt @@ -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 } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UrlEncoder.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UrlEncoder.ios.kt index 665bf4925..3b8e98f02 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UrlEncoder.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UrlEncoder.ios.kt @@ -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) } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/cache/LargeCache.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/cache/LargeCache.ios.kt index a0881410c..bcec6b77b 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/cache/LargeCache.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/cache/LargeCache.ios.kt @@ -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 : ICacheOperations { - actual fun keys(): Set { - TODO("Not yet implemented") - } + private val concurrentMap = cacheMapOf() - actual fun values(): Iterable { - TODO("Not yet implemented") - } + actual fun keys(): Set = concurrentMap.keys - actual fun get(key: K): V? { - TODO("Not yet implemented") - } + actual fun values(): Iterable = 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) { - TODO("Not yet implemented") + concurrentMap.forEach { consumer.accept(it.key, it.value) } } - actual override fun filter(consumer: CacheCollectors.BiFilter): List { - TODO("Not yet implemented") - } + actual override fun filter(consumer: CacheCollectors.BiFilter): List = + concurrentMap + .filter { consumer.filter(it.key, it.value) } + .values + .toList() - actual override fun filterIntoSet(consumer: CacheCollectors.BiFilter): Set { - TODO("Not yet implemented") - } + actual override fun filterIntoSet(consumer: CacheCollectors.BiFilter): Set = + concurrentMap + .filter { consumer.filter(it.key, it.value) } + .values + .toSet() - actual override fun map(consumer: CacheCollectors.BiNotNullMapper): List { - TODO("Not yet implemented") - } + actual override fun map(consumer: CacheCollectors.BiNotNullMapper): List = concurrentMap.map { consumer.map(it.key, it.value) } - actual override fun mapNotNull(consumer: CacheCollectors.BiMapper): List { - TODO("Not yet implemented") - } + actual override fun mapNotNull(consumer: CacheCollectors.BiMapper): List = concurrentMap.mapNotNull { consumer.map(it.key, it.value) } - actual override fun mapNotNullIntoSet(consumer: CacheCollectors.BiMapper): Set { - TODO("Not yet implemented") - } + actual override fun mapNotNullIntoSet(consumer: CacheCollectors.BiMapper): Set = mapNotNull(consumer).toSet() - actual override fun mapFlatten(consumer: CacheCollectors.BiMapper?>): List { - TODO("Not yet implemented") - } + actual override fun mapFlatten(consumer: CacheCollectors.BiMapper?>): List = concurrentMap.flatMap { entry -> consumer.map(entry.key, entry.value) ?: emptyList() } - actual override fun mapFlattenIntoSet(consumer: CacheCollectors.BiMapper?>): Set { - TODO("Not yet implemented") - } + actual override fun mapFlattenIntoSet(consumer: CacheCollectors.BiMapper?>): Set = mapFlatten(consumer).toSet() actual override fun maxOrNullOf( filter: CacheCollectors.BiFilter, comparator: Comparator, ): 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): 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): Long { - TODO("Not yet implemented") - } + actual override fun sumOfLong(consumer: CacheCollectors.BiSumOfLong): Long = concurrentMap.sumOfLong(consumer) - actual override fun groupBy(consumer: CacheCollectors.BiNotNullMapper): Map> { - TODO("Not yet implemented") - } + actual override fun groupBy(consumer: CacheCollectors.BiNotNullMapper): Map> = concurrentMap.groupBy(consumer) - actual override fun countByGroup(consumer: CacheCollectors.BiNotNullMapper): Map { - TODO("Not yet implemented") - } + actual override fun countByGroup(consumer: CacheCollectors.BiNotNullMapper): Map = concurrentMap.countByGroup(consumer) actual override fun sumByGroup( groupMap: CacheCollectors.BiNotNullMapper, sumOf: CacheCollectors.BiNotNullMapper, - ): Map { - TODO("Not yet implemented") - } + ): Map = concurrentMap.sumByGroup(groupMap, sumOf) - actual override fun count(consumer: CacheCollectors.BiFilter): Int { - TODO("Not yet implemented") - } + actual override fun count(consumer: CacheCollectors.BiFilter): Int = concurrentMap.count { consumer.filter(it.key, it.value) } - actual override fun associate(transform: (K, V) -> Pair): Map { - TODO("Not yet implemented") - } + actual override fun associate(transform: (K, V) -> Pair): Map = concurrentMap.associate(transform) - actual override fun associateWith(transform: (K, V) -> U?): Map { - TODO("Not yet implemented") - } + actual override fun associateWith(transform: (K, V) -> U?): Map = concurrentMap.associateWith(transform) actual override fun filter( from: K, to: K, consumer: CacheCollectors.BiFilter, ): List { - 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, - ): Set { - TODO("Not yet implemented") - } + ): Set = filter(from, to, consumer).toSet() actual override fun map( from: K, to: K, consumer: CacheCollectors.BiNotNullMapper, ): List { - TODO("Not yet implemented") + val transientList = concurrentMap.subMapAlt(from, to) + return transientList.map { consumer.map(it.key, it.value) } } actual override fun mapNotNull( from: K, to: K, consumer: CacheCollectors.BiMapper, - ): List { - TODO("Not yet implemented") - } + ): List = concurrentMap.subMapAlt(from, to).mapNotNull { consumer.map(it.key, it.value) } actual override fun mapNotNullIntoSet( from: K, to: K, consumer: CacheCollectors.BiMapper, - ): Set { - TODO("Not yet implemented") - } + ): Set = mapNotNull(from, to, consumer).toSet() actual override fun mapFlatten( from: K, to: K, consumer: CacheCollectors.BiMapper?>, - ): List { - TODO("Not yet implemented") - } + ): List = concurrentMap.subMapAlt(from, to).flatMap { consumer.map(it.key, it.value) as Iterable } actual override fun mapFlattenIntoSet( from: K, to: K, consumer: CacheCollectors.BiMapper?>, - ): Set { - TODO("Not yet implemented") - } + ): Set = mapFlatten(from, to, consumer).toSet() actual override fun maxOrNullOf( from: K, @@ -209,73 +201,58 @@ actual class LargeCache : ICacheOperations { filter: CacheCollectors.BiFilter, comparator: Comparator, ): 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, - ): Int { - TODO("Not yet implemented") - } + ): Int = concurrentMap.subMapAlt(from, to).sumOf(consumer) actual override fun sumOfLong( from: K, to: K, consumer: CacheCollectors.BiSumOfLong, - ): Long { - TODO("Not yet implemented") - } + ): Long = concurrentMap.subMapAlt(from, to).sumOfLong(consumer) actual override fun groupBy( from: K, to: K, consumer: CacheCollectors.BiNotNullMapper, - ): Map> { - TODO("Not yet implemented") - } + ): Map> = concurrentMap.subMapAlt(from, to).groupBy(consumer) actual override fun countByGroup( from: K, to: K, consumer: CacheCollectors.BiNotNullMapper, - ): Map { - TODO("Not yet implemented") - } + ): Map = concurrentMap.subMapAlt(from, to).countByGroup(consumer) actual override fun sumByGroup( from: K, to: K, groupMap: CacheCollectors.BiNotNullMapper, sumOf: CacheCollectors.BiNotNullMapper, - ): Map { - TODO("Not yet implemented") - } + ): Map = concurrentMap.subMapAlt(from, to).sumByGroup(groupMap, sumOf) actual override fun count( from: K, to: K, consumer: CacheCollectors.BiFilter, - ): Int { - TODO("Not yet implemented") - } + ): Int = concurrentMap.subMapAlt(from, to).count { consumer.filter(it.key, it.value) } actual override fun associate( from: K, to: K, transform: (K, V) -> Pair, - ): Map { - TODO("Not yet implemented") - } + ): Map = concurrentMap.subMapAlt(from, to).associate(transform) actual override fun associateWith( from: K, to: K, transform: (K, V) -> U?, - ): Map { - TODO("Not yet implemented") - } + ): Map = concurrentMap.subMapAlt(from, to).associateWith(transform) actual override fun joinToString( separator: CharSequence, @@ -285,6 +262,172 @@ actual class LargeCache : ICacheOperations { 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 CacheMap.subMapSlow( + from: K, + to: K, + toInclusive: Boolean = true, +): Map { + 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 CacheMap.subMapAlt( + from: K, + to: K, + toInclusive: Boolean = true, +): Map { + val resultMap = hashMapOf() + 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 Map.maxOrNullOf( + filter: CacheCollectors.BiFilter, + comparator: Comparator, +): 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 Map.sumOf(consumer: CacheCollectors.BiSumOf): Int { + var sum = 0 + forEach { sum += consumer.map(it.key, it.value) } + return sum +} + +fun Map.sumOfLong(consumer: CacheCollectors.BiSumOfLong): Long { + var sum = 0L + forEach { sum += consumer.map(it.key, it.value) } + return sum +} + +fun Map.groupBy(consumer: CacheCollectors.BiNotNullMapper): Map> { + val results = HashMap>() + forEach { + val group = consumer.map(it.key, it.value) + val list = results[group] + if (list == null) { + val answer = ArrayList() + answer.add(it.value) + results[group] = answer + } else { + list.add(it.value) + } + } + + return results +} + +fun Map.countByGroup(consumer: CacheCollectors.BiNotNullMapper): Map { + val results = HashMap() + 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 Map.sumByGroup( + groupMap: CacheCollectors.BiNotNullMapper, + sumOf: CacheCollectors.BiNotNullMapper, +): Map { + val results = HashMap() + 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 Map.associate(transform: (K, V) -> Pair): Map { + val results: LinkedHashMap = LinkedHashMap(size) + forEach { + val pair = transform(it.key, it.value) + results[pair.first] = pair.second + } + + return results +} + +fun Map.associateWith(transform: (K, V) -> U?): Map { + val results: LinkedHashMap = LinkedHashMap(size) + forEach { + results[it.key] = transform(it.key, it.value) + } + + return results +} diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESCBC.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESCBC.ios.kt index 9eb3aedce..4be429a12 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESCBC.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESCBC.ios.kt @@ -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 + } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt index aed72dad3..eb065059a 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt @@ -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 + } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/diggest/DigestInstance.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/diggest/DigestInstance.ios.kt index 5ef8ff2e8..fec92dcc1 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/diggest/DigestInstance.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/diggest/DigestInstance.ios.kt @@ -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.") + } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Buffer.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Buffer.kt new file mode 100644 index 000000000..8db20ceda --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Buffer.kt @@ -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 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 newPosition 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 newLimit 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 true 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 true 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: + * + *
+     * buf.clear();     // Prepare buffer for reading
+     * in.read(buf);    // Read data
+ * + * + * 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: + * + *
+     * 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
+ * + * + * 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 { + 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: + * + *
+     * out.write(buf);    // Write remaining data
+     * buf.rewind();      // Rewind buffer
+     * buf.get(array);    // Copy data into array
+ * + * @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 + + // -- 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)", + ) + } + } + } +} diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/ByteBuffers.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/ByteBuffers.kt new file mode 100644 index 000000000..7f82d60ab --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/ByteBuffers.kt @@ -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 constructor( + capacity: Int, + order: ByteOrder = ByteOrder.LittleEndian, + override val isReadOnly: Boolean = false, +) : Buffer(-1, 0, capacity, capacity, order), + Comparable> { + 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  (optional operation). + * + *

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 p = position() is copied + * to index zero, the byte at index p + 1 is copied + * to index one, and so forth until the byte at index + * limit() - 1 is copied to index + * n = limit() - 1 - p. + * The buffer's position is then set to n+1 and its limit is set to + * its capacity. The mark, if defined, is discarded. + * + *

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 put + * method.

+ * + + * + *

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 buf: + * + *

{@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
+     *   }
+     * }
+ * + * @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. + * + *

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): 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) { + 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 + } + + /** + *

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 + * src.remaining() > remaining(), + * then no bytes are transferred and an IllegalStateException is thrown + *

+ * + *

Otherwise, this method copies + * n = src.remaining() bytes from the given + * buffer into this buffer, starting at each buffer's current position. + * The positions of both buffers are then incremented by n. + */ + open fun put(source: ByteBufferBase) { + 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(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. + * + *

Two byte buffers are equal if, and only if, + * + *

    + * + *
  1. They have the same element type,

  2. + * + *
  3. They have the same number of remaining elements, and + *

  4. + * + *
  5. The two sequences of remaining elements, considered + * independently of their starting positions, are pointwise equal. + + *

  6. + * + *
+ * + *

A byte buffer is not equal to any other type of object.

+ * + * @param other The object to which this buffer is to be compared + * + * @return true 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. + * + *

The hash code of a byte buffer depends only upon its remaining + * elements; that is, upon the elements from position() up to, and + * including, the element at limit() - 1. + * + *

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.

+ * + * @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(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. + * + *

Two byte buffers are equal if, and only if, + * + *

    + * + *
  1. They have the same element type,

  2. + * + *
  3. They have the same number of remaining elements, and + *

  4. + * + *
  5. The two sequences of remaining elements, considered + * independently of their starting positions, are pointwise equal. + + *

  6. + * + *
+ * + *

A byte buffer is not equal to any other type of object.

+ * + * @param other The object to which this buffer is to be compared + * + * @return true 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. + * + *

The hash code of a byte buffer depends only upon its remaining + * elements; that is, upon the elements from position() up to, and + * including, the element at limit() - 1. + * + *

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.

+ * + * @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 + } +} diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt new file mode 100644 index 000000000..b148010c2 --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt @@ -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) + } + } +} diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt new file mode 100644 index 000000000..75d892e59 --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt @@ -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) + } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt index f1990a734..52b65b39e 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt @@ -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.") + } } diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt index 493c189e2..00c7bda27 100644 --- a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt @@ -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.") } } diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt new file mode 100644 index 000000000..b34458b6b --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt @@ -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", + ), + ) + } +} diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt new file mode 100644 index 000000000..baa144ebe --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt @@ -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) + } +} diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt new file mode 100644 index 000000000..e41063c7b --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt @@ -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) + } +} diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt new file mode 100644 index 000000000..bc2dfa1c7 --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt @@ -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()) + } +} diff --git a/quartz/src/iosTest/resources/ovxxk2vz.jpg b/quartz/src/iosTest/resources/ovxxk2vz.jpg new file mode 100644 index 000000000..bc96672af Binary files /dev/null and b/quartz/src/iosTest/resources/ovxxk2vz.jpg differ diff --git a/quartz/src/iosTest/resources/trouble_video b/quartz/src/iosTest/resources/trouble_video new file mode 100644 index 000000000..401d5e1df Binary files /dev/null and b/quartz/src/iosTest/resources/trouble_video differ diff --git a/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt b/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt index fe331449b..bb522a8b7 100644 --- a/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt +++ b/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt @@ -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") } diff --git a/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift b/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift new file mode 100644 index 000000000..bafd34a5b --- /dev/null +++ b/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift @@ -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! + } +} diff --git a/quartz/src/swift/swiftbridge/UrlDetector.swift b/quartz/src/swift/swiftbridge/UrlDetector.swift new file mode 100644 index 000000000..e4927c516 --- /dev/null +++ b/quartz/src/swift/swiftbridge/UrlDetector.swift @@ -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 + } +} \ No newline at end of file