-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into 'master'
Develop See merge request papers/airgap/airgap-vault!412
- Loading branch information
Showing
140 changed files
with
8,084 additions
and
1,305 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
android/app/src/main/java/it/airgap/vault/plugin/isolatedmodules/FileExplorer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package it.airgap.vault.plugin.isolatedmodules | ||
|
||
import android.content.Context | ||
import com.getcapacitor.JSObject | ||
import it.airgap.vault.plugin.isolatedmodules.js.JSModule | ||
import it.airgap.vault.plugin.isolatedmodules.js.environment.JSEnvironment | ||
import it.airgap.vault.util.Directory | ||
import it.airgap.vault.util.getDirectory | ||
import it.airgap.vault.util.readBytes | ||
import java.io.File | ||
|
||
interface StaticSourcesExplorer { | ||
fun readJavaScriptEngineUtils(): ByteArray | ||
fun readIsolatedModulesScript(): ByteArray | ||
} | ||
|
||
interface DynamicSourcesExplorer<in M : JSModule> { | ||
fun listModules(): List<String> | ||
|
||
fun readModuleSources(module: M): Sequence<ByteArray> | ||
fun readModuleManifest(module: String): ByteArray | ||
} | ||
|
||
private const val MANIFEST_FILENAME = "manifest.json" | ||
|
||
class FileExplorer private constructor( | ||
private val context: Context, | ||
private val assetsExplorer: AssetsExplorer, | ||
private val filesExplorer: FilesExplorer, | ||
) : StaticSourcesExplorer by assetsExplorer { | ||
constructor(context: Context) : this(context, AssetsExplorer(context), FilesExplorer(context)) | ||
|
||
fun loadAssetModules(): List<JSModule> = loadModules(assetsExplorer, JSModule::Asset) | ||
|
||
fun loadInstalledModules(): List<JSModule> = loadModules(filesExplorer, JSModule::Installed) | ||
|
||
fun loadInstalledModule(identifier: String): JSModule { | ||
val manifest = JSObject(filesExplorer.readModuleManifest(identifier).decodeToString()) | ||
|
||
return loadModule(identifier, manifest, JSModule::Installed) | ||
} | ||
|
||
fun loadPreviewModule(path: String, directory: Directory): JSModule { | ||
val moduleDir = File(context.getDirectory(directory), path) | ||
val manifest = JSObject(File(moduleDir, MANIFEST_FILENAME).readBytes().decodeToString()) | ||
|
||
return loadModule(moduleDir.name, manifest) { identifier, namespace, preferredEnvironment, paths -> | ||
JSModule.Preview( | ||
identifier, | ||
namespace, | ||
preferredEnvironment, | ||
paths, | ||
moduleDir.absolutePath, | ||
) | ||
} | ||
} | ||
|
||
fun readModuleSources(module: JSModule): Sequence<ByteArray> = | ||
when (module) { | ||
is JSModule.Asset -> assetsExplorer.readModuleSources(module) | ||
is JSModule.Installed -> filesExplorer.readModuleSources(module) | ||
is JSModule.Preview -> module.sources.asSequence().map { File(module.path, it).readBytes() } | ||
} | ||
|
||
fun readModuleManifest(module: JSModule): ByteArray = | ||
when (module) { | ||
is JSModule.Asset -> assetsExplorer.readModuleManifest(module.identifier) | ||
is JSModule.Installed -> filesExplorer.readModuleManifest(module.identifier) | ||
is JSModule.Preview -> File(module.path, MANIFEST_FILENAME).readBytes() | ||
} | ||
|
||
private fun <T : JSModule> loadModules( | ||
explorer: DynamicSourcesExplorer<T>, | ||
constructor: (identifier: String, namespace: String?, preferredEnvironment: JSEnvironment.Type, paths: List<String>) -> T, | ||
): List<T> = explorer.listModules().map { module -> | ||
val manifest = JSObject(explorer.readModuleManifest(module).decodeToString()) | ||
loadModule(module, manifest, constructor) | ||
} | ||
|
||
private fun <T : JSModule> loadModule( | ||
identifier: String, | ||
manifest: JSObject, | ||
constructor: (identifier: String, namespace: String?, preferredEnvironment: JSEnvironment.Type, paths: List<String>) -> T, | ||
): T { | ||
val namespace = manifest.getJSObject("src")?.getString("namespace") | ||
val preferredEnvironment = manifest.getJSObject("jsenv")?.getString("android")?.let { JSEnvironment.Type.fromString(it) } ?: JSEnvironment.Type.JavaScriptEngine | ||
val sources = buildList { | ||
val include = manifest.getJSONArray("include") | ||
for (i in 0 until include.length()) { | ||
val source = include.getString(i).takeIf { it.endsWith(".js") } ?: continue | ||
add(source.trimStart('/')) | ||
} | ||
} | ||
|
||
return constructor(identifier, namespace, preferredEnvironment, sources) | ||
} | ||
} | ||
|
||
private class AssetsExplorer(private val context: Context) : StaticSourcesExplorer, DynamicSourcesExplorer<JSModule.Asset> { | ||
override fun readJavaScriptEngineUtils(): ByteArray = context.assets.readBytes(JAVA_SCRIPT_ENGINE_UTILS) | ||
override fun readIsolatedModulesScript(): ByteArray = context.assets.readBytes(SCRIPT) | ||
|
||
override fun listModules(): List<String> = context.assets.list(MODULES_DIR)?.toList() ?: emptyList() | ||
|
||
override fun readModuleSources(module: JSModule.Asset): Sequence<ByteArray> = | ||
module.sources.asSequence().map { context.assets.readBytes(modulePath(module.identifier, it))} | ||
override fun readModuleManifest(module: String): ByteArray = context.assets.readBytes(modulePath(module, MANIFEST_FILENAME)) | ||
|
||
private fun modulePath(module: String, path: String): String = | ||
"${MODULES_DIR}/${module.trimStart('/')}/${path.trimStart('/')}" | ||
|
||
companion object { | ||
private const val SCRIPT = "public/assets/native/isolated_modules/isolated-modules.script.js" | ||
private const val JAVA_SCRIPT_ENGINE_UTILS = "public/assets/native/isolated_modules/isolated-modules.js-engine-android.js" | ||
|
||
private const val MODULES_DIR = "public/assets/protocol_modules" | ||
} | ||
} | ||
|
||
private class FilesExplorer(private val context: Context) : DynamicSourcesExplorer<JSModule.Installed> { | ||
private val modulesDir: File | ||
get() = File(context.filesDir, MODULES_DIR) | ||
|
||
override fun listModules(): List<String> = modulesDir.list()?.toList() ?: emptyList() | ||
|
||
override fun readModuleSources(module: JSModule.Installed): Sequence<ByteArray> = | ||
module.sources.asSequence().map { File(modulesDir, modulePath(module.identifier, it)).readBytes() } | ||
override fun readModuleManifest(module: String): ByteArray = File(modulesDir, modulePath(module, MANIFEST_FILENAME)).readBytes() | ||
|
||
private fun modulePath(module: String, path: String): String = | ||
"${module.trimStart('/')}/${path.trimStart('/')}" | ||
|
||
companion object { | ||
private const val MODULES_DIR = "protocol_modules" | ||
} | ||
} |
157 changes: 157 additions & 0 deletions
157
android/app/src/main/java/it/airgap/vault/plugin/isolatedmodules/IsolatedModules.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package it.airgap.vault.plugin.isolatedmodules | ||
|
||
import androidx.lifecycle.lifecycleScope | ||
import com.getcapacitor.JSArray | ||
import com.getcapacitor.JSObject | ||
import com.getcapacitor.Plugin | ||
import com.getcapacitor.PluginCall | ||
import com.getcapacitor.PluginMethod | ||
import com.getcapacitor.annotation.CapacitorPlugin | ||
import it.airgap.vault.plugin.isolatedmodules.js.* | ||
import it.airgap.vault.util.* | ||
import kotlinx.coroutines.Deferred | ||
import kotlinx.coroutines.launch | ||
|
||
@CapacitorPlugin | ||
class IsolatedModules : Plugin() { | ||
private val jsEvaluator: Deferred<JSEvaluator> = ExecutableDeferred { JSEvaluator(context, fileExplorer) } | ||
private val fileExplorer: FileExplorer by lazy { FileExplorer(context) } | ||
|
||
@PluginMethod | ||
fun previewModule(call: PluginCall) { | ||
call.executeCatching { | ||
assertReceived(Param.PATH, Param.DIRECTORY) | ||
|
||
activity.lifecycleScope.launch { | ||
executeCatching { | ||
val module = fileExplorer.loadPreviewModule(path, directory) | ||
val manifest = fileExplorer.readModuleManifest(module) | ||
val moduleJson = jsEvaluator.await().evaluatePreviewModule(module) | ||
|
||
resolve( | ||
JSObject( | ||
""" | ||
{ | ||
"module": $moduleJson, | ||
"manifest": ${JSObject(manifest.decodeToString())} | ||
} | ||
""".trimIndent() | ||
) | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
@PluginMethod | ||
fun registerModule(call: PluginCall) { | ||
call.executeCatching { | ||
assertReceived(Param.IDENTIFIER, Param.PROTOCOL_IDENTIFIERS) | ||
|
||
activity.lifecycleScope.launch { | ||
executeCatching { | ||
val module = fileExplorer.loadInstalledModule(identifier) | ||
jsEvaluator.await().registerModule(module, protocolIdentifiers) | ||
|
||
resolve() | ||
} | ||
} | ||
} | ||
} | ||
|
||
@PluginMethod | ||
fun loadModules(call: PluginCall) { | ||
activity.lifecycleScope.launch { | ||
call.executeCatching { | ||
val modules = fileExplorer.loadAssetModules() + fileExplorer.loadInstalledModules() | ||
|
||
resolve(jsEvaluator.await().evaluateLoadModules(modules, protocolType)) | ||
} | ||
} | ||
} | ||
|
||
@PluginMethod | ||
fun callMethod(call: PluginCall) { | ||
call.executeCatching { | ||
assertReceived(Param.TARGET, Param.METHOD) | ||
|
||
activity.lifecycleScope.launch { | ||
executeCatching { | ||
val value = when (target) { | ||
JSCallMethodTarget.OfflineProtocol -> { | ||
assertReceived(Param.PROTOCOL_IDENTIFIER) | ||
jsEvaluator.await().evaluateCallOfflineProtocolMethod(method, args, protocolIdentifier) | ||
} | ||
JSCallMethodTarget.OnlineProtocol -> { | ||
assertReceived(Param.PROTOCOL_IDENTIFIER) | ||
jsEvaluator.await().evaluateCallOnlineProtocolMethod(method, args, protocolIdentifier, networkId) | ||
} | ||
JSCallMethodTarget.BlockExplorer -> { | ||
assertReceived(Param.PROTOCOL_IDENTIFIER) | ||
jsEvaluator.await().evaluateCallBlockExplorerMethod(method, args, protocolIdentifier, networkId) | ||
} | ||
JSCallMethodTarget.V3SerializerCompanion -> { | ||
assertReceived(Param.MODULE_IDENTIFIER) | ||
jsEvaluator.await().evaluateCallV3SerializerCompanionMethod(method, args, moduleIdentifier) | ||
} | ||
} | ||
resolve(value) | ||
} | ||
} | ||
} | ||
} | ||
|
||
override fun handleOnDestroy() { | ||
super.handleOnDestroy() | ||
activity.lifecycleScope.launch { | ||
jsEvaluator.await().destroy() | ||
} | ||
} | ||
|
||
private val PluginCall.path: String | ||
get() = getString(Param.PATH)!! | ||
|
||
private val PluginCall.directory: Directory | ||
get() = getString(Param.DIRECTORY)?.let { Directory.fromString(it) }!! | ||
|
||
private val PluginCall.identifier: String | ||
get() = getString(Param.IDENTIFIER)!! | ||
|
||
private val PluginCall.protocolIdentifiers: List<String> | ||
get() = getArray(Param.PROTOCOL_IDENTIFIERS).toList() | ||
|
||
private val PluginCall.protocolType: JSProtocolType? | ||
get() = getString(Param.PROTOCOL_TYPE)?.let { JSProtocolType.fromString(it) } | ||
|
||
private val PluginCall.target: JSCallMethodTarget | ||
get() = getString(Param.TARGET)?.let { JSCallMethodTarget.fromString(it) }!! | ||
|
||
private val PluginCall.method: String | ||
get() = getString(Param.METHOD)!! | ||
|
||
private val PluginCall.args: JSArray? | ||
get() = getArray(Param.ARGS, null) | ||
|
||
private val PluginCall.protocolIdentifier: String | ||
get() = getString(Param.PROTOCOL_IDENTIFIER)!! | ||
|
||
private val PluginCall.moduleIdentifier: String | ||
get() = getString(Param.MODULE_IDENTIFIER)!! | ||
|
||
private val PluginCall.networkId: String? | ||
get() = getString(Param.NETWORK_ID) | ||
|
||
private object Param { | ||
const val PATH = "path" | ||
const val DIRECTORY = "directory" | ||
const val IDENTIFIER = "identifier" | ||
const val PROTOCOL_IDENTIFIERS = "protocolIdentifiers" | ||
const val PROTOCOL_TYPE = "protocolType" | ||
const val TARGET = "target" | ||
const val METHOD = "method" | ||
const val ARGS = "args" | ||
const val PROTOCOL_IDENTIFIER = "protocolIdentifier" | ||
const val MODULE_IDENTIFIER = "moduleIdentifier" | ||
const val NETWORK_ID = "networkId" | ||
} | ||
} |
Oops, something went wrong.