working API level 1
This commit is contained in:
parent
b214ddb6c9
commit
e3a0522e87
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/.gradle/
|
||||||
|
/.idea/
|
||||||
|
build
|
78
build.gradle.kts
Normal file
78
build.gradle.kts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
val ktor_version: String by project
|
||||||
|
val kotlin_version: String by project
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "1.7.10"
|
||||||
|
kotlin("plugin.serialization") version "1.7.10"
|
||||||
|
`maven-publish`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "net.sergeych"
|
||||||
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven("https://maven.universablockchain.com")
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
compilations.all {
|
||||||
|
kotlinOptions.jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
withJava()
|
||||||
|
testRuns["test"].executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
js(IR) {
|
||||||
|
browser {
|
||||||
|
commonWebpackConfig {
|
||||||
|
cssSupport.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// val hostOs = System.getProperty("os.name")
|
||||||
|
// val isMingwX64 = hostOs.startsWith("Windows")
|
||||||
|
// val nativeTarget = when {
|
||||||
|
// hostOs == "Mac OS X" -> macosX64("native")
|
||||||
|
// hostOs == "Linux" -> linuxX64("native")
|
||||||
|
// isMingwX64 -> mingwX64("native")
|
||||||
|
// else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3")
|
||||||
|
// implementation("org.jetbrains.kotlin:atomicfu:1.6.21")
|
||||||
|
implementation("io.ktor:ktor-client-core:$ktor_version")
|
||||||
|
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
|
||||||
|
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
|
||||||
|
api("net.sergeych:unikrypto:1.1.1-SNAPSHOT")
|
||||||
|
api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.2")
|
||||||
|
api("net.sergeych:mp_stools:1.2.3-SNAPSHOT")
|
||||||
|
api("net.sergeych:boss-serialization-mp:0.1.2-SNAPSHOT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val commonTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation("io.ktor:ktor-client-cio:$ktor_version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmTest by getting
|
||||||
|
val jsMain by getting
|
||||||
|
val jsTest by getting
|
||||||
|
// val nativeMain by getting
|
||||||
|
// val nativeTest by getting
|
||||||
|
}
|
||||||
|
}
|
6
gradle.properties
Normal file
6
gradle.properties
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
kotlin.code.style=official
|
||||||
|
kotlin.mpp.enableGranularSourceSetsMetadata=true
|
||||||
|
kotlin.native.enableDependencyPropagation=false
|
||||||
|
kotlin.js.generate.executable.default=false
|
||||||
|
ktor_version=2.0.3
|
||||||
|
kotlin_version=1.7.10
|
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
2232
kotlin-js-store/yarn.lock
Normal file
2232
kotlin-js-store/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
3
settings.gradle.kts
Normal file
3
settings.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
rootProject.name = "crypstie3api"
|
||||||
|
|
18
src/commonMain/kotlin/crypstie3/ApiCrypstie.kt
Normal file
18
src/commonMain/kotlin/crypstie3/ApiCrypstie.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package crypstie3
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiCrypstie(
|
||||||
|
val guid: String,
|
||||||
|
val encryptedData: ByteArray,
|
||||||
|
val deleteAt: Instant = Clock.System.now() + 90.days,
|
||||||
|
val burnOnShow: Boolean = false,
|
||||||
|
val showWide: Boolean = false,
|
||||||
|
val syntaxHighlighting: String? = null,
|
||||||
|
val editable: Boolean = false,
|
||||||
|
val ownerHandle: String? = null,
|
||||||
|
)
|
167
src/commonMain/kotlin/crypstie3/Client.kt
Normal file
167
src/commonMain/kotlin/crypstie3/Client.kt
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
|
|
||||||
|
package crypstie3
|
||||||
|
|
||||||
|
import api.ApiError
|
||||||
|
import api.ErrorResult
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.request.forms.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import net.sergeych.boss_serialization_mp.BossEncoder
|
||||||
|
import net.sergeych.boss_serialization_mp.decodeBoss
|
||||||
|
import net.sergeych.mp_tools.decodeBase64Compact
|
||||||
|
import net.sergeych.mp_tools.encodeToBase64Compact
|
||||||
|
import net.sergeych.mp_tools.globalLaunch
|
||||||
|
import net.sergeych.mptools.CachedExpression
|
||||||
|
import net.sergeych.unikrypto.SymmetricKeys
|
||||||
|
import tools.decodeBase64Url
|
||||||
|
import tools.encodeToBase64Url
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
|
class CrypstieClient(val rootUrl: String) {
|
||||||
|
|
||||||
|
private val hc = CompletableDeferred<HttpClient>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
globalLaunch {
|
||||||
|
try {
|
||||||
|
hc.complete(HttpClient() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (x: Exception) {
|
||||||
|
hc.completeExceptionally(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cachedVersion = CachedExpression<String>()
|
||||||
|
|
||||||
|
suspend fun version(): String {
|
||||||
|
return cachedVersion.get {
|
||||||
|
hc.await()
|
||||||
|
.get("$rootUrl/api/version")
|
||||||
|
.body<VersionInfo>()
|
||||||
|
.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create encrypted crypstie on the server and return it as a path part of the
|
||||||
|
* of the secret url, e.g. `_guid#haskey`. You should prepend protocol and host to your
|
||||||
|
* web service to use it or whatever else but leave has part as a hash to not to compromise
|
||||||
|
* your data!
|
||||||
|
*/
|
||||||
|
suspend fun createPath(
|
||||||
|
text: String,
|
||||||
|
deleteAt: Instant = Clock.System.now() + 90.days,
|
||||||
|
burnOnShow: Boolean = false,
|
||||||
|
ownerHandle: String? = null,
|
||||||
|
) = createPath(text.toByteArray(), deleteAt, burnOnShow, ownerHandle = ownerHandle)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create encrypted crypstie and return its path-patr of the url, e.g. `_guid#haskey`
|
||||||
|
* just add your interface host to it
|
||||||
|
*/
|
||||||
|
suspend fun createPath(
|
||||||
|
data: ByteArray,
|
||||||
|
deleteAt: Instant = Clock.System.now() + 90.days,
|
||||||
|
burnOnShow: Boolean = false,
|
||||||
|
profileIds: List<String>? = null,
|
||||||
|
useWide: Boolean = false,
|
||||||
|
syntaxHighlighting: String? = null,
|
||||||
|
editable: Boolean = false,
|
||||||
|
ownerHandle: String? = null,
|
||||||
|
): String {
|
||||||
|
// Generate random key and encrypt data
|
||||||
|
val k = SymmetricKeys.random()
|
||||||
|
val encrypted = k.etaEncrypt(data)
|
||||||
|
val keyAsString = k.keyBytes.encodeToBase64Url()
|
||||||
|
|
||||||
|
// post crypstie and return URL from the service
|
||||||
|
val response = hc.await().submitFormWithBinaryData(
|
||||||
|
url = "$rootUrl/api/crypstie",
|
||||||
|
formData = formData {
|
||||||
|
append(
|
||||||
|
"data",
|
||||||
|
BossEncoder.encode(
|
||||||
|
ApiCrypstie(
|
||||||
|
"",
|
||||||
|
encrypted, deleteAt, burnOnShow, useWide,
|
||||||
|
syntaxHighlighting, editable, ownerHandle
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, "application/octet-stream")
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=\"dummy.bin\"")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val guid = checkResponse(response).body<String>()
|
||||||
|
// now complete it with a key:
|
||||||
|
return "_$guid#$keyAsString"
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkResponse(response: HttpResponse): HttpResponse {
|
||||||
|
if (!response.status.isSuccess()) {
|
||||||
|
if (response.status.value == 404)
|
||||||
|
ApiError.INTERNAL_ERROR.raise("404: not found")
|
||||||
|
try {
|
||||||
|
response.body()
|
||||||
|
} catch (x: Exception) {
|
||||||
|
ErrorResult(ApiError.UNKNOWN_ERROR, x.toString())
|
||||||
|
}.raise()
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* donwload and decrypt crypstie by its url path part with hashtah
|
||||||
|
* @param pathPart path part of the url e.g. `_<id>#<encoded_key>`.
|
||||||
|
* @throws ApiError if can't download/decrypt.
|
||||||
|
*/
|
||||||
|
suspend fun decryptPath(pathPart: String, profileIds: List<String>? = null): DecryptedCrypstie {
|
||||||
|
if (pathPart[0] != '_') ApiError.BAD_PARAMETER.raise("pathPart should start with #")
|
||||||
|
val parts = pathPart.split('#')
|
||||||
|
|
||||||
|
fun invalidFormat(text: String? = null): Nothing {
|
||||||
|
ApiError.BAD_PARAMETER.raise(
|
||||||
|
"invalid pathPart format${text?.let { ": $it" } ?: ""} "
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank())
|
||||||
|
invalidFormat()
|
||||||
|
val key = try {
|
||||||
|
val keyBytes = parts[1].decodeBase64Url()
|
||||||
|
if (keyBytes.size != 32) invalidFormat("wrong hashkey size")
|
||||||
|
SymmetricKeys.create(keyBytes)
|
||||||
|
} catch (x: Exception) {
|
||||||
|
invalidFormat("failed to decode hashkey: $x")
|
||||||
|
}
|
||||||
|
|
||||||
|
val query = if (profileIds.isNullOrEmpty()) ""
|
||||||
|
else "?pids=${profileIds.joinToString(",")}"
|
||||||
|
val packed = checkResponse(hc.await().get("$rootUrl/api/crypstie/${parts[0].substring(1)}$query"))
|
||||||
|
.body<ByteArray>()
|
||||||
|
val ac = packed.decodeBoss<ApiCrypstie>()
|
||||||
|
try {
|
||||||
|
return DecryptedCrypstie(key, ac)
|
||||||
|
} catch (x: Exception) {
|
||||||
|
invalidFormat("failed to decrypt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
src/commonMain/kotlin/crypstie3/DecryptedCrypstie.kt
Normal file
17
src/commonMain/kotlin/crypstie3/DecryptedCrypstie.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package crypstie3
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import net.sergeych.unikrypto.SymmetricKey
|
||||||
|
|
||||||
|
class DecryptedCrypstie(
|
||||||
|
val guid: String,
|
||||||
|
val data: ByteArray,
|
||||||
|
val deleteAt: Instant,
|
||||||
|
val burnOnShow: Boolean,
|
||||||
|
val ownerHandle: String?
|
||||||
|
) {
|
||||||
|
constructor(key: SymmetricKey, ac: ApiCrypstie)
|
||||||
|
: this(ac.guid, key.etaDecrypt(ac.encryptedData), ac.deleteAt, ac.burnOnShow, ac.ownerHandle)
|
||||||
|
|
||||||
|
val text by lazy { data.decodeToString() }
|
||||||
|
}
|
27
src/commonMain/kotlin/crypstie3/errors.kt
Normal file
27
src/commonMain/kotlin/crypstie3/errors.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
enum class ApiError {
|
||||||
|
UNKNOWN_ERROR,
|
||||||
|
BAD_PARAMETER,
|
||||||
|
INTERNAL_ERROR,
|
||||||
|
INVALID_DELETE_DATE,
|
||||||
|
DATA_TOO_BIG,
|
||||||
|
NOT_FOUND;
|
||||||
|
|
||||||
|
fun raise(text: String? = null): Nothing {
|
||||||
|
throw ApiException(this, text ?: defaultText())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultText(): String = name.lowercase().replace('_', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiException(val code: ApiError, _text: String? = null, cause: Throwable? = null) :
|
||||||
|
Exception(_text ?: code.defaultText(), cause) {
|
||||||
|
val text by lazy { _text ?: code.defaultText() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class ErrorResult(val code: ApiError,val text: String) {
|
||||||
|
fun raise(): Nothing { throw ApiException(code, text) }
|
||||||
|
}
|
72
src/commonMain/kotlin/crypstie3/tools/SimpleStorage.kt
Normal file
72
src/commonMain/kotlin/crypstie3/tools/SimpleStorage.kt
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
interface BackingStorge {
|
||||||
|
fun getItem(key: String): String?
|
||||||
|
fun setItem(key: String, value: String)
|
||||||
|
fun clearItem(key: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TempBackingStorage : BackingStorge {
|
||||||
|
|
||||||
|
private val m = mutableMapOf<String,String>()
|
||||||
|
|
||||||
|
override fun getItem(key: String): String? = m[key]
|
||||||
|
|
||||||
|
override fun setItem(key: String, value: String) {
|
||||||
|
m[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearItem(key: String) {
|
||||||
|
m.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrefixedStorage(private val bs: BackingStorge, val prefix: String): BackingStorge {
|
||||||
|
|
||||||
|
override fun getItem(key: String): String? {
|
||||||
|
return bs.getItem(prefix + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setItem(key: String, value: String) {
|
||||||
|
bs.setItem(prefix+key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearItem(key: String) {
|
||||||
|
bs.clearItem(prefix+key)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val format = Json { prettyPrint = false }
|
||||||
|
|
||||||
|
inline fun <reified T>storage(backingStorge: BackingStorge,overrideName: String?=null): SimpleStorageDelegate<T> {
|
||||||
|
return SimpleStorageDelegate(backingStorge, overrideName, serializer())
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleStorageDelegate<T>(
|
||||||
|
val storage: BackingStorge,
|
||||||
|
val overrideName: String? = null,
|
||||||
|
val sr: KSerializer<T>,
|
||||||
|
) {
|
||||||
|
private var cacheIsSet = false
|
||||||
|
private var cachedValue: T? = null
|
||||||
|
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? =
|
||||||
|
if (cacheIsSet)
|
||||||
|
cachedValue
|
||||||
|
else
|
||||||
|
storage.getItem(overrideName ?: property.name)?.let { format.decodeFromString(sr, it) }
|
||||||
|
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
|
||||||
|
val name = overrideName ?: property.name
|
||||||
|
if (value == null) storage.clearItem(name)
|
||||||
|
else storage.setItem(name, format.encodeToString(sr, value))
|
||||||
|
cachedValue = value
|
||||||
|
cacheIsSet = true
|
||||||
|
}
|
||||||
|
}
|
43
src/commonMain/kotlin/crypstie3/tools/catchApi.kt
Normal file
43
src/commonMain/kotlin/crypstie3/tools/catchApi.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import api.ApiError
|
||||||
|
import api.ApiException
|
||||||
|
|
||||||
|
class CatchApiResult<T>(_result: T?, val exception: ApiException?) {
|
||||||
|
var errorIsProcessed = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
var result = _result
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun on(vararg errors: ApiError, block: suspend () -> T?): CatchApiResult<T> {
|
||||||
|
if (exception != null && exception.code in errors) {
|
||||||
|
errorIsProcessed = true
|
||||||
|
result = block()
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ignore(vararg errors: ApiError): CatchApiResult<T> {
|
||||||
|
if (exception != null && exception.code in errors)
|
||||||
|
errorIsProcessed = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun orThrowError(): CatchApiResult<T> {
|
||||||
|
if (exception != null && !errorIsProcessed) throw exception
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(): T? = orThrowError().result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun <T> catchApi(block: suspend () -> T): CatchApiResult<T> =
|
||||||
|
try {
|
||||||
|
CatchApiResult(block(), null)
|
||||||
|
} catch (e: ApiException) {
|
||||||
|
CatchApiResult(null, e)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
CatchApiResult(null, ApiException(ApiError.UNKNOWN_ERROR, cause= t))
|
||||||
|
}
|
21
src/commonMain/kotlin/crypstie3/tools/random_tools.kt
Normal file
21
src/commonMain/kotlin/crypstie3/tools/random_tools.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
private const val lowerLetters = "qwertyuiopasdfghjklzxcvbnm"
|
||||||
|
private val idFirstLetters: String = lowerLetters + lowerLetters.uppercase() + "_"
|
||||||
|
private val idLetters: String = idFirstLetters + "1234567890-"
|
||||||
|
|
||||||
|
val CharSequence.sampleChar: Char
|
||||||
|
get() = this[Random.nextInt(0,length)]
|
||||||
|
|
||||||
|
val <T> List<T>.sample: T
|
||||||
|
get() = this[Random.nextInt(0,size)]
|
||||||
|
|
||||||
|
fun randomId(length: Int): String {
|
||||||
|
if( length < 2 ) throw IllegalArgumentException("too short")
|
||||||
|
val result = StringBuilder(idFirstLetters.sampleChar.toString())
|
||||||
|
for( i in 1 until length ) result.append(idLetters.sampleChar)
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
30
src/commonMain/kotlin/crypstie3/tools/simple_tools.kt
Normal file
30
src/commonMain/kotlin/crypstie3/tools/simple_tools.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import net.sergeych.mp_tools.decodeBase64
|
||||||
|
import net.sergeych.mp_tools.decodeBase64Compact
|
||||||
|
import net.sergeych.mp_tools.encodeToBase64Compact
|
||||||
|
|
||||||
|
suspend fun ignoreErrors(block: suspend ()->Unit) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
catch(x: CancellationException) {
|
||||||
|
throw x
|
||||||
|
}
|
||||||
|
catch(t: Throwable) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.trimOrNull(): String?{
|
||||||
|
val s = trim()
|
||||||
|
return if( s == "" ) null else s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.decodeBase64Url(): ByteArray = replace('.', '+')
|
||||||
|
.replace('_', '/')
|
||||||
|
.decodeBase64Compact()
|
||||||
|
|
||||||
|
fun ByteArray.encodeToBase64Url(): String = encodeToBase64Compact()
|
||||||
|
.replace('+', '.')
|
||||||
|
.replace('/', '_')
|
||||||
|
|
7
src/commonMain/kotlin/crypstie3/version.kt
Normal file
7
src/commonMain/kotlin/crypstie3/version.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package crypstie3
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VersionInfo(val version: String,val service: String)
|
||||||
|
|
14
src/commonTest/kotlin/CrypstieClientTest.kt
Normal file
14
src/commonTest/kotlin/CrypstieClientTest.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import crypstie3.CrypstieClient
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
internal class CrypstieClientTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun create() = runTest {
|
||||||
|
val client = CrypstieClient("http://localhost:8296")
|
||||||
|
assertEquals(3, client.version().split('.').size)
|
||||||
|
val x = client.createPath("foobarbazz")
|
||||||
|
val dc = client.decryptPath(x)
|
||||||
|
assertEquals("foobarbazz", dc.text)
|
||||||
|
}
|
||||||
|
}
|
4
src/commonTest/kotlin/runTest.kt
Normal file
4
src/commonTest/kotlin/runTest.kt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import net.sergeych.mp_tools.globalLaunch
|
||||||
|
|
||||||
|
expect fun runTest(block: suspend () -> Unit)
|
||||||
|
|
4
src/jsTest/kotlin/runTest.kt
Normal file
4
src/jsTest/kotlin/runTest.kt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.promise
|
||||||
|
|
||||||
|
actual fun runTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() }
|
6
src/jvmMain/kotlin/date_tools.kt
Normal file
6
src/jvmMain/kotlin/date_tools.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.toKotlinInstant
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
fun ZonedDateTime.toKotlinInstant(): Instant = toInstant().toKotlinInstant()
|
||||||
|
|
5
src/jvmTest/kotlin/runTest.kt
Normal file
5
src/jvmTest/kotlin/runTest.kt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
actual fun runTest(block: suspend () -> Unit) {
|
||||||
|
runBlocking { block() }
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user