diff --git a/code/go-gopherjs/.gitignore b/code/go-gopherjs/.gitignore
new file mode 100644
index 000000000..98c957c64
--- /dev/null
+++ b/code/go-gopherjs/.gitignore
@@ -0,0 +1,2 @@
+*.js.map
+*.js
diff --git a/code/go-gopherjs/Makefile b/code/go-gopherjs/Makefile
new file mode 100644
index 000000000..391e4f09c
--- /dev/null
+++ b/code/go-gopherjs/Makefile
@@ -0,0 +1,7 @@
+include ../go/common.mk
+
+build:
+ gopherjs build -o validate.lib.js .
+
+test:
+ @echo "no tests"
diff --git a/code/go-gopherjs/main.go b/code/go-gopherjs/main.go
new file mode 100644
index 000000000..8c115cd66
--- /dev/null
+++ b/code/go-gopherjs/main.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+ "bytes"
+
+ "github.com/gopherjs/gopherjs/js"
+
+ "github.com/elastic/package-spec/v2/code/go/pkg/validator"
+)
+
+const moduleName = "elasticPackageSpec"
+
+func main() {
+ js.Global.Set(moduleName, make(map[string]interface{}))
+ module := js.Global.Get(moduleName)
+ module.Set("validateFromBuffer", func(name string, buffer []byte) string {
+ reader := bytes.NewReader(buffer)
+ err := validator.ValidateFromZipReader(name, reader, int64(len(buffer)))
+ if err != nil {
+ return err.Error()
+ }
+ return ""
+ })
+}
diff --git a/code/go-gopherjs/package.json b/code/go-gopherjs/package.json
new file mode 100644
index 000000000..3c8aa666b
--- /dev/null
+++ b/code/go-gopherjs/package.json
@@ -0,0 +1,6 @@
+{
+ "dependencies": {
+ "source-map-support": "^0.5.21",
+ "syscall": "/home/jaime/gocode/src/github.com/gopherjs/gopherjs/node-syscall"
+ }
+}
diff --git a/code/go-gopherjs/validate.js b/code/go-gopherjs/validate.js
new file mode 100755
index 000000000..1a7fee8ea
--- /dev/null
+++ b/code/go-gopherjs/validate.js
@@ -0,0 +1,17 @@
+#!/usr/bin/env node
+
+if (process.argv.length < 3) {
+ console.error("usage: " + process.argv[1] + " [package path]");
+ process.exit(1);
+}
+
+
+require('./validate.lib.js');
+
+const fs = require('fs');
+
+const packagePath = process.argv[2];
+const packageBuffer = fs.readFileSync(packagePath);
+
+var result = elasticPackageSpec.validateFromBuffer(packagePath, packageBuffer);
+console.log(result);
diff --git a/code/go-wasm/.gitignore b/code/go-wasm/.gitignore
new file mode 100644
index 000000000..19e1bced9
--- /dev/null
+++ b/code/go-wasm/.gitignore
@@ -0,0 +1 @@
+*.wasm
diff --git a/code/go-wasm/Makefile b/code/go-wasm/Makefile
new file mode 100644
index 000000000..e3fce217b
--- /dev/null
+++ b/code/go-wasm/Makefile
@@ -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/...
diff --git a/code/go-wasm/README.md b/code/go-wasm/README.md
new file mode 100644
index 000000000..2b32b5af6
--- /dev/null
+++ b/code/go-wasm/README.md
@@ -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.
diff --git a/code/go-wasm/index.html b/code/go-wasm/index.html
new file mode 100644
index 000000000..a0177fc5d
--- /dev/null
+++ b/code/go-wasm/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/code/go-wasm/main.go b/code/go-wasm/main.go
new file mode 100644
index 000000000..397897148
--- /dev/null
+++ b/code/go-wasm/main.go
@@ -0,0 +1,95 @@
+// 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/v2/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].Type() != js.TypeString {
+ return fmt.Errorf("string expected")
+ }
+ if len(args) < 2 || args[1].Type() != js.TypeNumber {
+ return fmt.Errorf("number expected")
+ }
+ if len(args) < 3 || !args[2].InstanceOf(js.Global().Get("Uint8Array")) {
+ return fmt.Errorf("array buffer with content of package expected")
+ }
+
+ name := args[0].String()
+ size := int64(args[1].Int())
+
+ buf := make([]byte, size)
+ js.CopyBytesToGo(buf, args[2])
+
+ 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
+}
diff --git a/code/go-wasm/server.go b/code/go-wasm/server.go
new file mode 100644
index 000000000..ad1547c85
--- /dev/null
+++ b/code/go-wasm/server.go
@@ -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))
+}
diff --git a/code/go-wasm/validate.js b/code/go-wasm/validate.js
new file mode 100755
index 000000000..dfba641d9
--- /dev/null
+++ b/code/go-wasm/validate.js
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+
+if (process.argv.length < 3) {
+ console.error("usage: " + process.argv[1] + " [package path]");
+ process.exit(1);
+}
+
+// From wasm_exec_node.js
+globalThis.require = require;
+globalThis.fs = require("fs");
+globalThis.TextEncoder = require("util").TextEncoder;
+globalThis.TextDecoder = require("util").TextDecoder;
+
+globalThis.performance = {
+ now() {
+ const [sec, nsec] = process.hrtime();
+ return sec * 1000 + nsec / 1000000;
+ },
+};
+
+const crypto = require("crypto");
+globalThis.crypto = {
+ getRandomValues(b) {
+ crypto.randomFillSync(b);
+ },
+};
+
+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);
+});
diff --git a/code/go-wasm/validate.lib.js b/code/go-wasm/validate.lib.js
new file mode 100644
index 000000000..9616032ba
--- /dev/null
+++ b/code/go-wasm/validate.lib.js
@@ -0,0 +1,23 @@
+var wasmBuffer;
+fetch('validator.wasm').then((response) => {
+ return response.arrayBuffer();
+}).then((buffer) => {
+ wasmBuffer = buffer;
+})
+const go = new Go();
+
+function validateZipBuffer(name, size, buffer, success, error) {
+ WebAssembly.instantiate(wasmBuffer, go.importObject).then((validator) => {
+ go.run(validator.instance);
+
+ elasticPackageSpec.validateFromZipReader(name, size, buffer).then(() =>
+ success()
+ ).catch((err) =>
+ error(err)
+ ).finally(() =>
+ elasticPackageSpec.stop()
+ )
+ }).catch((err) => {
+ console.error(err);
+ });
+}
diff --git a/code/go-wasm/wasm_exec.js b/code/go-wasm/wasm_exec.js
new file mode 100644
index 000000000..9ce6a20c3
--- /dev/null
+++ b/code/go-wasm/wasm_exec.js
@@ -0,0 +1,554 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+"use strict";
+
+(() => {
+ const enosys = () => {
+ const err = new Error("not implemented");
+ err.code = "ENOSYS";
+ return err;
+ };
+
+ if (!globalThis.fs) {
+ let outputBuf = "";
+ globalThis.fs = {
+ constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
+ writeSync(fd, buf) {
+ outputBuf += decoder.decode(buf);
+ const nl = outputBuf.lastIndexOf("\n");
+ if (nl != -1) {
+ console.log(outputBuf.substr(0, nl));
+ outputBuf = outputBuf.substr(nl + 1);
+ }
+ return buf.length;
+ },
+ write(fd, buf, offset, length, position, callback) {
+ if (offset !== 0 || length !== buf.length || position !== null) {
+ callback(enosys());
+ return;
+ }
+ const n = this.writeSync(fd, buf);
+ callback(null, n);
+ },
+ chmod(path, mode, callback) { callback(enosys()); },
+ chown(path, uid, gid, callback) { callback(enosys()); },
+ close(fd, callback) { callback(enosys()); },
+ fchmod(fd, mode, callback) { callback(enosys()); },
+ fchown(fd, uid, gid, callback) { callback(enosys()); },
+ fstat(fd, callback) { callback(enosys()); },
+ fsync(fd, callback) { callback(null); },
+ ftruncate(fd, length, callback) { callback(enosys()); },
+ lchown(path, uid, gid, callback) { callback(enosys()); },
+ link(path, link, callback) { callback(enosys()); },
+ lstat(path, callback) { callback(enosys()); },
+ mkdir(path, perm, callback) { callback(enosys()); },
+ open(path, flags, mode, callback) { callback(enosys()); },
+ read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
+ readdir(path, callback) { callback(enosys()); },
+ readlink(path, callback) { callback(enosys()); },
+ rename(from, to, callback) { callback(enosys()); },
+ rmdir(path, callback) { callback(enosys()); },
+ stat(path, callback) { callback(enosys()); },
+ symlink(path, link, callback) { callback(enosys()); },
+ truncate(path, length, callback) { callback(enosys()); },
+ unlink(path, callback) { callback(enosys()); },
+ utimes(path, atime, mtime, callback) { callback(enosys()); },
+ };
+ }
+
+ if (!globalThis.process) {
+ globalThis.process = {
+ getuid() { return -1; },
+ getgid() { return -1; },
+ geteuid() { return -1; },
+ getegid() { return -1; },
+ getgroups() { throw enosys(); },
+ pid: -1,
+ ppid: -1,
+ umask() { throw enosys(); },
+ cwd() { throw enosys(); },
+ chdir() { throw enosys(); },
+ }
+ }
+
+ if (!globalThis.crypto) {
+ throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
+ }
+
+ if (!globalThis.performance) {
+ throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
+ }
+
+ if (!globalThis.TextEncoder) {
+ throw new Error("globalThis.TextEncoder is not available, polyfill required");
+ }
+
+ if (!globalThis.TextDecoder) {
+ throw new Error("globalThis.TextDecoder is not available, polyfill required");
+ }
+
+ const encoder = new TextEncoder("utf-8");
+ const decoder = new TextDecoder("utf-8");
+
+ globalThis.Go = class {
+ constructor() {
+ this.argv = ["js"];
+ this.env = {};
+ this.exit = (code) => {
+ if (code !== 0) {
+ console.warn("exit code:", code);
+ }
+ };
+ this._exitPromise = new Promise((resolve) => {
+ this._resolveExitPromise = resolve;
+ });
+ this._pendingEvent = null;
+ this._scheduledTimeouts = new Map();
+ this._nextCallbackTimeoutID = 1;
+
+ const setInt64 = (addr, v) => {
+ this.mem.setUint32(addr + 0, v, true);
+ this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
+ }
+
+ const getInt64 = (addr) => {
+ const low = this.mem.getUint32(addr + 0, true);
+ const high = this.mem.getInt32(addr + 4, true);
+ return low + high * 4294967296;
+ }
+
+ const loadValue = (addr) => {
+ const f = this.mem.getFloat64(addr, true);
+ if (f === 0) {
+ return undefined;
+ }
+ if (!isNaN(f)) {
+ return f;
+ }
+
+ const id = this.mem.getUint32(addr, true);
+ return this._values[id];
+ }
+
+ const storeValue = (addr, v) => {
+ const nanHead = 0x7FF80000;
+
+ if (typeof v === "number" && v !== 0) {
+ if (isNaN(v)) {
+ this.mem.setUint32(addr + 4, nanHead, true);
+ this.mem.setUint32(addr, 0, true);
+ return;
+ }
+ this.mem.setFloat64(addr, v, true);
+ return;
+ }
+
+ if (v === undefined) {
+ this.mem.setFloat64(addr, 0, true);
+ return;
+ }
+
+ let id = this._ids.get(v);
+ if (id === undefined) {
+ id = this._idPool.pop();
+ if (id === undefined) {
+ id = this._values.length;
+ }
+ this._values[id] = v;
+ this._goRefCounts[id] = 0;
+ this._ids.set(v, id);
+ }
+ this._goRefCounts[id]++;
+ let typeFlag = 0;
+ switch (typeof v) {
+ case "object":
+ if (v !== null) {
+ typeFlag = 1;
+ }
+ break;
+ case "string":
+ typeFlag = 2;
+ break;
+ case "symbol":
+ typeFlag = 3;
+ break;
+ case "function":
+ typeFlag = 4;
+ break;
+ }
+ this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
+ this.mem.setUint32(addr, id, true);
+ }
+
+ const loadSlice = (addr) => {
+ const array = getInt64(addr + 0);
+ const len = getInt64(addr + 8);
+ return new Uint8Array(this._inst.exports.mem.buffer, array, len);
+ }
+
+ const loadSliceOfValues = (addr) => {
+ const array = getInt64(addr + 0);
+ const len = getInt64(addr + 8);
+ const a = new Array(len);
+ for (let i = 0; i < len; i++) {
+ a[i] = loadValue(array + i * 8);
+ }
+ return a;
+ }
+
+ const loadString = (addr) => {
+ const saddr = getInt64(addr + 0);
+ const len = getInt64(addr + 8);
+ return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
+ }
+
+ const timeOrigin = Date.now() - performance.now();
+ this.importObject = {
+ go: {
+ // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
+ // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
+ // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
+ // This changes the SP, thus we have to update the SP used by the imported function.
+
+ // func wasmExit(code int32)
+ "runtime.wasmExit": (sp) => {
+ sp >>>= 0;
+ const code = this.mem.getInt32(sp + 8, true);
+ this.exited = true;
+ delete this._inst;
+ delete this._values;
+ delete this._goRefCounts;
+ delete this._ids;
+ delete this._idPool;
+ this.exit(code);
+ },
+
+ // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
+ "runtime.wasmWrite": (sp) => {
+ sp >>>= 0;
+ const fd = getInt64(sp + 8);
+ const p = getInt64(sp + 16);
+ const n = this.mem.getInt32(sp + 24, true);
+ fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
+ },
+
+ // func resetMemoryDataView()
+ "runtime.resetMemoryDataView": (sp) => {
+ sp >>>= 0;
+ this.mem = new DataView(this._inst.exports.mem.buffer);
+ },
+
+ // func nanotime1() int64
+ "runtime.nanotime1": (sp) => {
+ sp >>>= 0;
+ setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
+ },
+
+ // func walltime() (sec int64, nsec int32)
+ "runtime.walltime": (sp) => {
+ sp >>>= 0;
+ const msec = (new Date).getTime();
+ setInt64(sp + 8, msec / 1000);
+ this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
+ },
+
+ // func scheduleTimeoutEvent(delay int64) int32
+ "runtime.scheduleTimeoutEvent": (sp) => {
+ sp >>>= 0;
+ const id = this._nextCallbackTimeoutID;
+ this._nextCallbackTimeoutID++;
+ this._scheduledTimeouts.set(id, setTimeout(
+ () => {
+ this._resume();
+ while (this._scheduledTimeouts.has(id)) {
+ // for some reason Go failed to register the timeout event, log and try again
+ // (temporary workaround for https://github.com/golang/go/issues/28975)
+ console.warn("scheduleTimeoutEvent: missed timeout event");
+ this._resume();
+ }
+ },
+ getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
+ ));
+ this.mem.setInt32(sp + 16, id, true);
+ },
+
+ // func clearTimeoutEvent(id int32)
+ "runtime.clearTimeoutEvent": (sp) => {
+ sp >>>= 0;
+ const id = this.mem.getInt32(sp + 8, true);
+ clearTimeout(this._scheduledTimeouts.get(id));
+ this._scheduledTimeouts.delete(id);
+ },
+
+ // func getRandomData(r []byte)
+ "runtime.getRandomData": (sp) => {
+ sp >>>= 0;
+ crypto.getRandomValues(loadSlice(sp + 8));
+ },
+
+ // func finalizeRef(v ref)
+ "syscall/js.finalizeRef": (sp) => {
+ sp >>>= 0;
+ const id = this.mem.getUint32(sp + 8, true);
+ this._goRefCounts[id]--;
+ if (this._goRefCounts[id] === 0) {
+ const v = this._values[id];
+ this._values[id] = null;
+ this._ids.delete(v);
+ this._idPool.push(id);
+ }
+ },
+
+ // func stringVal(value string) ref
+ "syscall/js.stringVal": (sp) => {
+ sp >>>= 0;
+ storeValue(sp + 24, loadString(sp + 8));
+ },
+
+ // func valueGet(v ref, p string) ref
+ "syscall/js.valueGet": (sp) => {
+ sp >>>= 0;
+ const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 32, result);
+ },
+
+ // func valueSet(v ref, p string, x ref)
+ "syscall/js.valueSet": (sp) => {
+ sp >>>= 0;
+ Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
+ },
+
+ // func valueDelete(v ref, p string)
+ "syscall/js.valueDelete": (sp) => {
+ sp >>>= 0;
+ Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
+ },
+
+ // func valueIndex(v ref, i int) ref
+ "syscall/js.valueIndex": (sp) => {
+ sp >>>= 0;
+ storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
+ },
+
+ // valueSetIndex(v ref, i int, x ref)
+ "syscall/js.valueSetIndex": (sp) => {
+ sp >>>= 0;
+ Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
+ },
+
+ // func valueCall(v ref, m string, args []ref) (ref, bool)
+ "syscall/js.valueCall": (sp) => {
+ sp >>>= 0;
+ try {
+ const v = loadValue(sp + 8);
+ const m = Reflect.get(v, loadString(sp + 16));
+ const args = loadSliceOfValues(sp + 32);
+ const result = Reflect.apply(m, v, args);
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 56, result);
+ this.mem.setUint8(sp + 64, 1);
+ } catch (err) {
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 56, err);
+ this.mem.setUint8(sp + 64, 0);
+ }
+ },
+
+ // func valueInvoke(v ref, args []ref) (ref, bool)
+ "syscall/js.valueInvoke": (sp) => {
+ sp >>>= 0;
+ try {
+ const v = loadValue(sp + 8);
+ const args = loadSliceOfValues(sp + 16);
+ const result = Reflect.apply(v, undefined, args);
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, result);
+ this.mem.setUint8(sp + 48, 1);
+ } catch (err) {
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, err);
+ this.mem.setUint8(sp + 48, 0);
+ }
+ },
+
+ // func valueNew(v ref, args []ref) (ref, bool)
+ "syscall/js.valueNew": (sp) => {
+ sp >>>= 0;
+ try {
+ const v = loadValue(sp + 8);
+ const args = loadSliceOfValues(sp + 16);
+ const result = Reflect.construct(v, args);
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, result);
+ this.mem.setUint8(sp + 48, 1);
+ } catch (err) {
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, err);
+ this.mem.setUint8(sp + 48, 0);
+ }
+ },
+
+ // func valueLength(v ref) int
+ "syscall/js.valueLength": (sp) => {
+ sp >>>= 0;
+ setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
+ },
+
+ // valuePrepareString(v ref) (ref, int)
+ "syscall/js.valuePrepareString": (sp) => {
+ sp >>>= 0;
+ const str = encoder.encode(String(loadValue(sp + 8)));
+ storeValue(sp + 16, str);
+ setInt64(sp + 24, str.length);
+ },
+
+ // valueLoadString(v ref, b []byte)
+ "syscall/js.valueLoadString": (sp) => {
+ sp >>>= 0;
+ const str = loadValue(sp + 8);
+ loadSlice(sp + 16).set(str);
+ },
+
+ // func valueInstanceOf(v ref, t ref) bool
+ "syscall/js.valueInstanceOf": (sp) => {
+ sp >>>= 0;
+ this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
+ },
+
+ // func copyBytesToGo(dst []byte, src ref) (int, bool)
+ "syscall/js.copyBytesToGo": (sp) => {
+ sp >>>= 0;
+ const dst = loadSlice(sp + 8);
+ const src = loadValue(sp + 32);
+ if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
+ this.mem.setUint8(sp + 48, 0);
+ return;
+ }
+ const toCopy = src.subarray(0, dst.length);
+ dst.set(toCopy);
+ setInt64(sp + 40, toCopy.length);
+ this.mem.setUint8(sp + 48, 1);
+ },
+
+ // func copyBytesToJS(dst ref, src []byte) (int, bool)
+ "syscall/js.copyBytesToJS": (sp) => {
+ sp >>>= 0;
+ const dst = loadValue(sp + 8);
+ const src = loadSlice(sp + 16);
+ if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
+ this.mem.setUint8(sp + 48, 0);
+ return;
+ }
+ const toCopy = src.subarray(0, dst.length);
+ dst.set(toCopy);
+ setInt64(sp + 40, toCopy.length);
+ this.mem.setUint8(sp + 48, 1);
+ },
+
+ "debug": (value) => {
+ console.log(value);
+ },
+ }
+ };
+ }
+
+ async run(instance) {
+ if (!(instance instanceof WebAssembly.Instance)) {
+ throw new Error("Go.run: WebAssembly.Instance expected");
+ }
+ this._inst = instance;
+ this.mem = new DataView(this._inst.exports.mem.buffer);
+ this._values = [ // JS values that Go currently has references to, indexed by reference id
+ NaN,
+ 0,
+ null,
+ true,
+ false,
+ globalThis,
+ this,
+ ];
+ this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
+ this._ids = new Map([ // mapping from JS values to reference ids
+ [0, 1],
+ [null, 2],
+ [true, 3],
+ [false, 4],
+ [globalThis, 5],
+ [this, 6],
+ ]);
+ this._idPool = []; // unused ids that have been garbage collected
+ this.exited = false; // whether the Go program has exited
+
+ // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
+ let offset = 4096;
+
+ const strPtr = (str) => {
+ const ptr = offset;
+ const bytes = encoder.encode(str + "\0");
+ new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
+ offset += bytes.length;
+ if (offset % 8 !== 0) {
+ offset += 8 - (offset % 8);
+ }
+ return ptr;
+ };
+
+ const argc = this.argv.length;
+
+ const argvPtrs = [];
+ this.argv.forEach((arg) => {
+ argvPtrs.push(strPtr(arg));
+ });
+ argvPtrs.push(0);
+
+ const keys = Object.keys(this.env).sort();
+ keys.forEach((key) => {
+ argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
+ });
+ argvPtrs.push(0);
+
+ const argv = offset;
+ argvPtrs.forEach((ptr) => {
+ this.mem.setUint32(offset, ptr, true);
+ this.mem.setUint32(offset + 4, 0, true);
+ offset += 8;
+ });
+
+ // The linker guarantees global data starts from at least wasmMinDataAddr.
+ // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
+ const wasmMinDataAddr = 4096 + 8192;
+ if (offset >= wasmMinDataAddr) {
+ throw new Error("total length of command line and environment variables exceeds limit");
+ }
+
+ this._inst.exports.run(argc, argv);
+ if (this.exited) {
+ this._resolveExitPromise();
+ }
+ await this._exitPromise;
+ }
+
+ _resume() {
+ if (this.exited) {
+ throw new Error("Go program has already exited");
+ }
+ this._inst.exports.resume();
+ if (this.exited) {
+ this._resolveExitPromise();
+ }
+ }
+
+ _makeFuncWrapper(id) {
+ const go = this;
+ return function () {
+ const event = { id: id, this: this, args: arguments };
+ go._pendingEvent = event;
+ go._resume();
+ return event.result;
+ };
+ }
+ }
+})();
diff --git a/code/go/Makefile b/code/go/Makefile
index cdf3186ab..f5cb2e57e 100644
--- a/code/go/Makefile
+++ b/code/go/Makefile
@@ -1,45 +1 @@
-SPEC_DIR=internal
-
-CODE_COVERAGE_REPORT_FOLDER = $(PWD)/build/test-coverage
-CODE_COVERAGE_REPORT_NAME_UNIT = $(CODE_COVERAGE_REPORT_FOLDER)/coverage-unit-report
-TEST_RESULTS_FOLDER = $(PWD)/build/test-results
-
-golicenser_cmd = go run github.com/elastic/go-licenser
-golint_cmd = go run golang.org/x/lint/golint
-gotestsum_cmd = go run gotest.tools/gotestsum
-gocobertura_cmd = go run github.com/boumenot/gocover-cobertura
-
-update:
- # Add license headers
- @$(golicenser_cmd) -license Elastic
-
-check: lint check-license check-spec
-
-# "yamlschema" directory has been excluded from linting, because it contains implementations of gojsonschema interfaces
-# which are not compliant with linter rules. The golint tool doesn't support ignore comments.
-lint:
- @go list ./... | grep -v yamlschema | xargs -n 1 $(golint_cmd) -set_exit_status
-
-check-license:
- @$(golicenser_cmd) -license Elastic -d
-
-# Checks that the spec is up-to-date
-check-spec:
- @$(golicenser_cmd) -license Elastic
-
-$(CODE_COVERAGE_REPORT_NAME_UNIT):
- mkdir -p $@
-
-$(TEST_RESULTS_FOLDER):
- mkdir -p $@
-
-# Runs tests
-test:
- # -count=1 is included to invalidate the test cache. This way, if you run "make test" multiple times
- # you will get fresh test results each time. For instance, changing the source of mocked packages
- # does not invalidate the cache so having the -count=1 to invalidate the test cache is useful.
- @go test -v ./... -count=1
-
-test-ci: $(CODE_COVERAGE_REPORT_NAME_UNIT) $(TEST_RESULTS_FOLDER)
- @$(gotestsum_cmd) --junitfile "$(TEST_RESULTS_FOLDER)/TEST-unit.xml" -- -count=1 -coverprofile=$(CODE_COVERAGE_REPORT_NAME_UNIT).out ./...
- @$(gocobertura_cmd) < $(CODE_COVERAGE_REPORT_NAME_UNIT).out > $(CODE_COVERAGE_REPORT_NAME_UNIT).xml
+include ./common.mk
diff --git a/code/go/common.mk b/code/go/common.mk
new file mode 100644
index 000000000..cdf3186ab
--- /dev/null
+++ b/code/go/common.mk
@@ -0,0 +1,45 @@
+SPEC_DIR=internal
+
+CODE_COVERAGE_REPORT_FOLDER = $(PWD)/build/test-coverage
+CODE_COVERAGE_REPORT_NAME_UNIT = $(CODE_COVERAGE_REPORT_FOLDER)/coverage-unit-report
+TEST_RESULTS_FOLDER = $(PWD)/build/test-results
+
+golicenser_cmd = go run github.com/elastic/go-licenser
+golint_cmd = go run golang.org/x/lint/golint
+gotestsum_cmd = go run gotest.tools/gotestsum
+gocobertura_cmd = go run github.com/boumenot/gocover-cobertura
+
+update:
+ # Add license headers
+ @$(golicenser_cmd) -license Elastic
+
+check: lint check-license check-spec
+
+# "yamlschema" directory has been excluded from linting, because it contains implementations of gojsonschema interfaces
+# which are not compliant with linter rules. The golint tool doesn't support ignore comments.
+lint:
+ @go list ./... | grep -v yamlschema | xargs -n 1 $(golint_cmd) -set_exit_status
+
+check-license:
+ @$(golicenser_cmd) -license Elastic -d
+
+# Checks that the spec is up-to-date
+check-spec:
+ @$(golicenser_cmd) -license Elastic
+
+$(CODE_COVERAGE_REPORT_NAME_UNIT):
+ mkdir -p $@
+
+$(TEST_RESULTS_FOLDER):
+ mkdir -p $@
+
+# Runs tests
+test:
+ # -count=1 is included to invalidate the test cache. This way, if you run "make test" multiple times
+ # you will get fresh test results each time. For instance, changing the source of mocked packages
+ # does not invalidate the cache so having the -count=1 to invalidate the test cache is useful.
+ @go test -v ./... -count=1
+
+test-ci: $(CODE_COVERAGE_REPORT_NAME_UNIT) $(TEST_RESULTS_FOLDER)
+ @$(gotestsum_cmd) --junitfile "$(TEST_RESULTS_FOLDER)/TEST-unit.xml" -- -count=1 -coverprofile=$(CODE_COVERAGE_REPORT_NAME_UNIT).out ./...
+ @$(gocobertura_cmd) < $(CODE_COVERAGE_REPORT_NAME_UNIT).out > $(CODE_COVERAGE_REPORT_NAME_UNIT).xml
diff --git a/code/go/pkg/validator/validator.go b/code/go/pkg/validator/validator.go
index 91a208d7c..9a0753717 100644
--- a/code/go/pkg/validator/validator.go
+++ b/code/go/pkg/validator/validator.go
@@ -8,6 +8,7 @@ import (
"archive/zip"
"errors"
"fmt"
+ "io"
"io/fs"
"os"
@@ -20,6 +21,16 @@ func ValidateFromPath(packageRootPath string) error {
return ValidateFromFS(packageRootPath, os.DirFS(packageRootPath))
}
+// ValidateFromZipReader validates a zip package obtained through a reader.
+func ValidateFromZipReader(location string, reader io.ReaderAt, size int64) error {
+ r, err := zip.NewReader(reader, size)
+ if err != nil {
+ return err
+ }
+
+ return validateZipReader(location, r)
+}
+
// ValidateFromZip validates a package on its zip format.
func ValidateFromZip(packagePath string) error {
r, err := zip.OpenReader(packagePath)
@@ -28,9 +39,13 @@ func ValidateFromZip(packagePath string) error {
}
defer r.Close()
+ return validateZipReader(packagePath, &r.Reader)
+}
+
+func validateZipReader(location string, r *zip.Reader) error {
dirs, err := fs.ReadDir(r, ".")
if err != nil {
- return fmt.Errorf("failed to read root directory in zip file (%s): %w", packagePath, err)
+ return fmt.Errorf("failed to read root directory in zip file (%s): %w", location, err)
}
if len(dirs) != 1 {
return fmt.Errorf("a single directory is expected in zip file, %d found", len(dirs))
@@ -41,7 +56,7 @@ func ValidateFromZip(packagePath string) error {
return err
}
- return ValidateFromFS(packagePath, subDir)
+ return ValidateFromFS(location, subDir)
}
// ValidateFromFS validates a package against the appropiate specification and returns any errors.
diff --git a/go.mod b/go.mod
index 2112b8e68..f4af71983 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
github.com/elastic/go-licenser v0.4.1
github.com/elastic/gojsonschema v1.2.1
github.com/evanphx/json-patch/v5 v5.6.0
+ github.com/gopherjs/gopherjs v1.17.2
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.1
diff --git a/go.sum b/go.sum
index e8691d28c..eb75514ed 100644
--- a/go.sum
+++ b/go.sum
@@ -29,6 +29,8 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=