Skip to content

Commit

Permalink
Add experimental support for wasmJs & wasmWasi (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
05nelsonm authored Mar 18, 2024
1 parent aed4d35 commit f46a0b5
Show file tree
Hide file tree
Showing 8 changed files with 1,706 additions and 96 deletions.
1,475 changes: 1,458 additions & 17 deletions .kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions build-logic/src/main/kotlin/-KmpConfigurationExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.matthewnelson.kmp.configuration.extension.KmpConfigurationExtension
import io.matthewnelson.kmp.configuration.extension.container.target.KmpConfigurationContainerDsl
import org.gradle.api.Action
import org.gradle.api.JavaVersion
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

fun KmpConfigurationExtension.configureShared(
publish: Boolean = false,
Expand All @@ -32,9 +33,25 @@ fun KmpConfigurationExtension.configureShared(
compileTargetCompatibility = JavaVersion.VERSION_1_8
}

js()
// wasmJs {}
// wasmWasi {}
js {
target {
browser()
nodejs()
}
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
target {
browser()
nodejs()
}
}
@OptIn(ExperimentalWasmDsl::class)
wasmWasi {
target {
nodejs()
}
}

androidNativeAll()

Expand Down
52 changes: 15 additions & 37 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension

plugins {
alias(libs.plugins.multiplatform) apply(false)
alias(libs.plugins.binaryCompat)
alias(libs.plugins.gradleVersions)
}

allprojects {
Expand All @@ -39,44 +40,21 @@ plugins.withType<YarnPlugin> {
the<YarnRootExtension>().lockFileDirectory = rootDir.resolve(".kotlin-js-store")
}

@Suppress("LocalVariableName")
apiValidation {
val CHECK_PUBLICATION = findProperty("CHECK_PUBLICATION") as? String

if (CHECK_PUBLICATION != null) {
ignoredProjects.add("check-publication")
} else {
ignoredProjects.add("sample")
plugins.withType<NodeJsRootPlugin> {
the<NodeJsRootExtension>().apply {
nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
}
}

fun isNonStable(version: String): Boolean {
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
val isStable = stableKeyword || regex.matches(version)
return isStable.not()
}

tasks.withType<DependencyUpdatesTask> {
// Example 1: reject all non stable versions
rejectVersionIf {
isNonStable(candidate.version)
}

// Example 2: disallow release candidates as upgradable versions from stable versions
rejectVersionIf {
isNonStable(candidate.version) && !isNonStable(currentVersion)
tasks.withType<KotlinNpmInstallTask>().configureEach {
args.add("--ignore-engines")
}
}

// Example 3: using the full syntax
resolutionStrategy {
componentSelection {
@Suppress("RedundantSamConstructor")
all(Action {
if (isNonStable(candidate.version) && !isNonStable(currentVersion)) {
reject("Release candidate")
}
})
}
apiValidation {
if (findProperty("CHECK_PUBLICATION") != null) {
ignoredProjects.add("check-publication")
} else {
ignoredProjects.add("sample")
}
}
12 changes: 5 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
[versions]
binaryCompat = "0.13.2"
configuration = "0.1.5"
coroutines = "1.7.3"
gradleVersions = "0.50.0"
kotlin = "1.9.21"
publish = "0.25.3"
binaryCompat = "0.14.0"
configuration = "0.2.1"
coroutines = "1.8.0"
kotlin = "1.9.23"
publish = "0.27.0"

[libraries]
gradle-kmp-configuration = { module = "io.matthewnelson:gradle-kmp-configuration-plugin", version.ref = "configuration" }
Expand All @@ -16,5 +15,4 @@ kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-te

[plugins]
binaryCompat = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompat" }
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

# https://gradle.org/release-checksums/
distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionSha256Sum=85719317abd2112f021d4f41f09ec370534ba288432065f4b477b6a3b652910d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
68 changes: 38 additions & 30 deletions secure-random/src/jsMain/kotlin/org/kotlincrypto/SecureRandom.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
* Copyright (c) 2023 Matthew Nelson
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
Expand All @@ -17,6 +17,7 @@

package org.kotlincrypto

import org.khronos.webgl.Int8Array
import org.kotlincrypto.internal.commonNextBytesOf
import org.kotlincrypto.internal.ifNotNullOrEmpty

Expand All @@ -38,51 +39,58 @@ public actual class SecureRandom public actual constructor() {
* Fills a [ByteArray] with securely generated random data.
* Does nothing if [bytes] is null or empty.
*
* Node: https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size
* Browser: https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues
* - [docs-node](https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size)
* - [docs-browser](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues)
*
* @throws [SecRandomCopyException] if procurement of securely random data failed.
* */
public actual fun nextBytesCopyTo(bytes: ByteArray?) {
bytes.ifNotNullOrEmpty {
try {
val array = unsafeCast<Int8Array>()

if (isNode) {
_require("crypto").randomFillSync(this)
crypto.randomFillSync(array)
} else {
global.crypto.getRandomValues(this)
var offset = 0
while (offset < size) {
val len = if (size > 65536) 65536 else size
crypto.getRandomValues(array.subarray(offset, offset + len))
offset += len
}
}

Unit
} catch (t: Throwable) {
throw SecRandomCopyException("Failed to obtain bytes", t)
}
}
}

private companion object {
private val isNode: Boolean by lazy {
val runtime: String? = try {
// May not be available, but should be preferred
// method of determining runtime environment.
js("(globalThis.process.release.name)") as String
} catch (_: Throwable) {
null
}
private val isNode: Boolean by lazy { isNodeJs() }
private val crypto: Crypto by lazy { if (isNode) cryptoNode() else cryptoBrowser() }
}
}

when (runtime) {
null -> {
js("(typeof global !== 'undefined' && ({}).toString.call(global) == '[object global]')") as Boolean
}
"node" -> true
else -> false
}
}
private fun cryptoNode(): Crypto = js("eval('require')('crypto')")
.unsafeCast<Crypto>()
private fun cryptoBrowser(): Crypto = js("(window ? (window.crypto ? window.crypto : window.msCrypto) : self.crypto)")
.unsafeCast<Crypto>()

private val global: dynamic by lazy {
js("((typeof global !== 'undefined') ? global : self)")
}
private fun isNodeJs(): Boolean = js(
"""
(typeof process !== 'undefined'
&& process.versions != null
&& process.versions.node != null) ||
(typeof window !== 'undefined'
&& typeof window.process !== 'undefined'
&& window.process.versions != null
&& window.process.versions.node != null)
"""
) as Boolean

@Suppress("FunctionName", "UNUSED_PARAMETER")
private fun _require(name: String): dynamic = js("require(name)")
}
private external class Crypto {
// Browser
fun getRandomValues(array: Int8Array)
// Node.js
fun randomFillSync(array: Int8Array)
}
101 changes: 101 additions & 0 deletions secure-random/src/wasmJsMain/kotlin/org/kotlincrypto/SecureRandom.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
@file:Suppress("ACTUAL_ANNOTATIONS_NOT_MATCH_EXPECT", "EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")

package org.kotlincrypto

import org.khronos.webgl.Int8Array
import org.khronos.webgl.get
import org.khronos.webgl.set
import org.kotlincrypto.internal.commonNextBytesOf
import org.kotlincrypto.internal.ifNotNullOrEmpty

/**
* A cryptographically strong random number generator (RNG).
* */
public actual class SecureRandom public actual constructor() {

/**
* Returns a [ByteArray] of size [count], filled with
* securely generated random data.
*
* @throws [IllegalArgumentException] if [count] is negative.
* @throws [SecRandomCopyException] if [nextBytesCopyTo] failed.
* */
public actual fun nextBytesOf(count: Int): ByteArray = commonNextBytesOf(count)

/**
* Fills a [ByteArray] with securely generated random data.
* Does nothing if [bytes] is null or empty.
*
* - [docs-node](https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size)
* - [docs-browser](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues)
*
* @throws [SecRandomCopyException] if procurement of securely random data failed.
* */
public actual fun nextBytesCopyTo(bytes: ByteArray?) {
bytes.ifNotNullOrEmpty {
try {
val array = Int8Array(size)

if (isNode) {
crypto.randomFillSync(array)
} else {
var offset = 0
while (offset < size) {
val len = if (size > 65536) 65536 else size
crypto.getRandomValues(array.subarray(offset, offset + len))
offset += len
}
}

for (i in indices) {
this[i] = array[i]
array[i] = 0
}
} catch (t: Throwable) {
throw SecRandomCopyException("Failed to obtain bytes", t)
}
}
}

private companion object {
private val isNode: Boolean by lazy { isNodeJs() }
private val crypto: Crypto by lazy { if (isNode) cryptoNode() else cryptoBrowser() }
}
}

private fun cryptoNode(): Crypto = js("eval('require')('crypto')")
private fun cryptoBrowser(): Crypto = js("(window ? (window.crypto ? window.crypto : window.msCrypto) : self.crypto)")

private fun isNodeJs(): Boolean = js(
"""
(typeof process !== 'undefined'
&& process.versions != null
&& process.versions.node != null) ||
(typeof window !== 'undefined'
&& typeof window.process !== 'undefined'
&& window.process.versions != null
&& window.process.versions.node != null)
"""
)

private external class Crypto: JsAny {
// Browser
fun getRandomValues(array: Int8Array)
// Node.js
fun randomFillSync(array: Int8Array)
}
Loading

0 comments on commit f46a0b5

Please sign in to comment.