Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] Validation of zip packages using javascript #285

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .ci/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pipeline {
stage('Test') {
steps {
cleanup()
installTools([ [tool: 'nodejs', version: '10.0' ] ])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work.

withMageEnv(){
dir("${BASE_DIR}"){
sh(label: 'Test', script: 'make test')
Expand Down
1 change: 1 addition & 0 deletions code/go-wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.wasm
7 changes: 7 additions & 0 deletions code/go-wasm/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include ../go/common.mk

build:
GOOS=js GOARCH=wasm go build -o validator.wasm .

test:
GOOS=js GOARCH=wasm go test -v -exec="`go env GOROOT`/misc/wasm/go_js_wasm_exec" . ../go/...
12 changes: 12 additions & 0 deletions code/go-wasm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## CLI

* Build wasm with `make build`.
* Run `./validate.js /path/to/package.zip` (or `node validate.js /path/to/package.zip`).

Node.js and Go are required.

## Web server with client validation

* Build wasm with `make build`.
* Run the server with `go run server.go`.
* Access http://localhost:8080 and select a file.
26 changes: 26 additions & 0 deletions code/go-wasm/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html>
<body>
<label for="package">Select a package</label><br />
<input name="package" type="file" id="package" />
<div id="result">
</div>

<script src="wasm_exec.js"></script>
<script src="validate.lib.js"></script>
<script>
const input = document.getElementById("package");
const result = document.getElementById("result");
input.addEventListener("change", function() {
const reader = new FileReader();
reader.onload = function(event) {
var buffer = new Uint8Array(reader.result);
validateZipBuffer(input.files[0], buffer,
() => { result.innerHTML = "Package is valid" },
(errors) => { result.innerHTML = errors.replaceAll("\n", "<br />") })
};
reader.readAsArrayBuffer(input.files[0]);
})
</script>
</body>
</html>
93 changes: 93 additions & 0 deletions code/go-wasm/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

//go:build wasm

package main

import (
"bytes"
"fmt"
"syscall/js"

"github.com/elastic/package-spec/code/go/pkg/validator"
)

const moduleName = "elasticPackageSpec"

// asyncFunc helps creating functions that return a promise.
//
// Calling async JavaScript APIs causes deadlocks in the JS event loop. Not sure
// how to find if a Go code does it, but for example ValidateFromZip does, so
// we need to run this code in a goroutine and return the result as a promise.
// Related: https://github.com/golang/go/issues/41310
func asyncFunc(fn func(this js.Value, args []js.Value) interface{}) js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
handler := js.FuncOf(func(_ js.Value, handlerArgs []js.Value) interface{} {
resolve := handlerArgs[0]
reject := handlerArgs[1]

go func() {
result := fn(this, args)
if err, ok := result.(error); ok {
reject.Invoke(err.Error())
return
}
resolve.Invoke(result)
}()

return nil
})

return js.Global().Get("Promise").New(handler)
})
}

func main() {
// It doesn't seem to be possible yet to export values as part of the compiled instance.
// So we have to expose it by setting a global value. It may worth to explore tinygo for this.
// Related: https://github.com/golang/go/issues/42372
js.Global().Set(moduleName, make(map[string]interface{}))
module := js.Global().Get(moduleName)
module.Set("validateFromZip", asyncFunc(
func(this js.Value, args []js.Value) interface{} {
if len(args) == 0 || args[0].IsNull() || args[0].IsUndefined() {
return fmt.Errorf("package path expected")
}

pkgPath := args[0].String()
return validator.ValidateFromZip(pkgPath)
},
))

module.Set("validateFromZipReader", asyncFunc(
func(this js.Value, args []js.Value) interface{} {
if len(args) < 1 || args[0].IsNull() || args[0].IsUndefined() {
return fmt.Errorf("file object expected")
}
if len(args) < 2 || args[1].IsNull() || args[1].IsUndefined() {
return fmt.Errorf("array buffer with content of package expected")
}

file := args[0]
name := file.Get("name").String()
size := int64(file.Get("size").Int())

buf := make([]byte, size)
js.CopyBytesToGo(buf, args[1])

reader := bytes.NewReader(buf)
return validator.ValidateFromZipReader(name, reader, size)
},
))

// Go runtime must be always available at any moment where exported functionality
// can be executed, so keep it running till done.
done := make(chan struct{})
module.Set("stop", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} {
close(done)
return nil
}))
<-done
}
18 changes: 18 additions & 0 deletions code/go-wasm/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

//go:build !wasm

package main

import (
"log"
"net/http"
)

func main() {
staticHandler := http.FileServer(http.Dir("."))
http.Handle("/", staticHandler)
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
27 changes: 27 additions & 0 deletions code/go-wasm/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node

if (process.argv.length < 3) {
console.error("usage: " + process.argv[1] + " [package path]");
process.exit(1);
}

const path = require('path')
const fs = require('fs');

require(path.join(process.env.GOROOT, "misc/wasm/wasm_exec.js"));
const go = new Go();

const wasmBuffer = fs.readFileSync('validator.wasm');
WebAssembly.instantiate(wasmBuffer, go.importObject).then((validator) => {
go.run(validator.instance);

elasticPackageSpec.validateFromZip(process.argv[2]).then(() =>
console.log("OK")
).catch((err) =>
console.error(err)
).finally(() =>
elasticPackageSpec.stop()
)
}).catch((err) => {
console.error(err);
});
23 changes: 23 additions & 0 deletions code/go-wasm/validate.lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var wasmBuffer;
fetch('validator.wasm').then((response) => {
return response.arrayBuffer();
}).then((buffer) => {
wasmBuffer = buffer;
})
const go = new Go();

function validateZipBuffer(file, buffer, success, error) {
WebAssembly.instantiate(wasmBuffer, go.importObject).then((validator) => {
go.run(validator.instance);

elasticPackageSpec.validateFromZipReader(file, buffer).then(() =>
success()
).catch((err) =>
error(err)
).finally(() =>
elasticPackageSpec.stop()
)
}).catch((err) => {
console.error(err);
});
}
Loading