Skip to content

Commit

Permalink
feat: add loadPolicySync
Browse files Browse the repository at this point in the history
  • Loading branch information
elliots committed Aug 27, 2022
1 parent 56c00e0 commit b3ebf5e
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 17 deletions.
114 changes: 98 additions & 16 deletions src/opa.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,24 +127,16 @@ function _builtinCall(
}

/**
* _loadPolicy can take in either an ArrayBuffer or WebAssembly.Module
* as its first argument, a WebAssembly.Memory for the second parameter,
* and an object mapping string names to additional builtin functions for
* the third parameter.
* It will return a Promise, depending on the input type the promise
* resolves to both a compiled WebAssembly.Module and its first WebAssembly.Instance
* or to the WebAssemblyInstance.
* @param {BufferSource | WebAssembly.Module} policyWasm
* _importObject builds the WebAssembly.Imports
* @param {Object} env
* @param {WebAssembly.Memory} memory
* @param {{ [builtinName: string]: Function }} customBuiltins
* @returns {Promise<{ policy: WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance, minorVersion: number }>}
* @returns {WebAssembly.Imports}
*/
async function _loadPolicy(policyWasm, memory, customBuiltins) {
function _importObject(env, memory, customBuiltins) {
const addr2string = stringDecoder(memory);

const env = {};

const wasm = await WebAssembly.instantiate(policyWasm, {
return {
env: {
memory,
opa_abort: function (addr) {
Expand Down Expand Up @@ -209,7 +201,17 @@ async function _loadPolicy(policyWasm, memory, customBuiltins) {
);
},
},
});
}
}

/**
* _preparePolicy checks the ABI version and loads the built-in functions
* @param {Object} env
* @param {WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance} wasm
* @param {WebAssembly.Memory} memory
* @returns { policy: WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance, minorVersion: number }}
*/
function _preparePolicy(env, wasm, memory) {

env.instance = wasm.instance ? wasm.instance : wasm;

Expand Down Expand Up @@ -253,6 +255,52 @@ async function _loadPolicy(policyWasm, memory, customBuiltins) {
return { policy: wasm, minorVersion: abiMinorVersion };
}

/**
* _loadPolicy can take in either an ArrayBuffer or WebAssembly.Module
* as its first argument, a WebAssembly.Memory for the second parameter,
* and an object mapping string names to additional builtin functions for
* the third parameter.
* It will return a Promise, depending on the input type the promise
* resolves to both a compiled WebAssembly.Module and its first WebAssembly.Instance
* or to the WebAssemblyInstance.
* @param {BufferSource | WebAssembly.Module} policyWasm
* @param {WebAssembly.Memory} memory
* @param {{ [builtinName: string]: Function }} customBuiltins
* @returns {Promise<{ policy: WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance, minorVersion: number }>}
*/
async function _loadPolicy(policyWasm, memory, customBuiltins) {

const env = {};

const wasm = await WebAssembly.instantiate(policyWasm, _importObject(env, memory, customBuiltins));

return _preparePolicy(env, wasm, memory);
}

/**
* _loadPolicySync can take in either an ArrayBuffer or WebAssembly.Module
* as its first argument, a WebAssembly.Memory for the second parameter,
* and an object mapping string names to additional builtin functions for
* the third parameter.
* It will return a compiled WebAssembly.Module and its first WebAssembly.Instance.
* @param {BufferSource | WebAssembly.Module} policyWasm
* @param {WebAssembly.Memory} memory
* @param {{ [builtinName: string]: Function }} customBuiltins
* @returns {Promise<{ policy: WebAssembly.Instance, minorVersion: number }>}
*/
function _loadPolicySync(policyWasm, memory, customBuiltins) {

const env = {};

if (policyWasm instanceof ArrayBuffer || policyWasm.buffer instanceof ArrayBuffer) {
policyWasm = new WebAssembly.Module(policyWasm);
}

const wasm = new WebAssembly.Instance(policyWasm, _importObject(env, memory, customBuiltins));

return _preparePolicy(env, wasm, memory);
}

/**
* LoadedPolicy is a wrapper around a WebAssembly.Instance and WebAssembly.Memory
* for a compiled Rego policy. There are helpers to run the wasm instance and
Expand Down Expand Up @@ -400,15 +448,16 @@ function roundup(bytes) {
module.exports = {
/**
* Takes in either an ArrayBuffer or WebAssembly.Module
* and will return a LoadedPolicy object which can be used to evaluate
* the policy.
* and will return a Promise of a LoadedPolicy object which
* can be used to evaluate the policy.
*
* To set custom memory size specify number of memory pages
* as second param.
* Defaults to 5 pages (320KB).
* @param {BufferSource | WebAssembly.Module} regoWasm
* @param {number | WebAssembly.MemoryDescriptor} memoryDescriptor For backwards-compatibility, a 'number' argument is taken to be the initial memory size.
* @param {{ [builtinName: string]: Function }} customBuiltins A map from string names to builtin functions
* @returns {Promise<LoadedPolicy>}
*/
async loadPolicy(regoWasm, memoryDescriptor = {}, customBuiltins = {}) {
// back-compat, second arg used to be a number: 'memorySize', with default of 5
Expand All @@ -425,4 +474,37 @@ module.exports = {
);
return new LoadedPolicy(policy, memory, minorVersion);
},

/**
* Takes in either an ArrayBuffer or WebAssembly.Module
* and will return a LoadedPolicy object which can be
* used to evaluate the policy.
*
* This cannot be used from the main thread in a browser.
* You must use the `loadPolicy` function instead, or call
* from a worker thread.
*
* To set custom memory size specify number of memory pages
* as second param.
* Defaults to 5 pages (320KB).
* @param {BufferSource | WebAssembly.Module} regoWasm
* @param {number | WebAssembly.MemoryDescriptor} memoryDescriptor For backwards-compatibility, a 'number' argument is taken to be the initial memory size.
* @param {{ [builtinName: string]: Function }} customBuiltins A map from string names to builtin functions
* @returns {LoadedPolicy}
*/
loadPolicySync(regoWasm, memoryDescriptor = {}, customBuiltins = {}) {
// back-compat, second arg used to be a number: 'memorySize', with default of 5
if (typeof memoryDescriptor === "number") {
memoryDescriptor = { initial: memoryDescriptor };
}
memoryDescriptor.initial = memoryDescriptor.initial || 5;

const memory = new WebAssembly.Memory(memoryDescriptor);
const { policy, minorVersion } = _loadPolicySync(
regoWasm,
memory,
customBuiltins,
);
return new LoadedPolicy(policy, memory, minorVersion);
},
};
16 changes: 16 additions & 0 deletions test/browser-integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ test("default script should expose working opa global", async () => {
]);
});

test("loadPolicySync can be used inside a worker thread", async () => {
const result = await page.evaluate(function () {
return new Promise ((resolve, _) => {
const worker = new Worker('/test/fixtures/load-policy-sync-worker.js');
worker.onmessage = function(e) {
resolve(e.data);
};
});
});
expect(result).toEqual([
{
result: { myOtherRule: false, myRule: false },
},
]);
});

async function startStaticServer() {
// Basic webserver to serve the test suite relative to the root.
const server = http.createServer(function (req, res) {
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/load-policy-sync-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
(async () => {
importScripts('/dist/opa-wasm-browser.js')

const wasm = await fetch('/test/fixtures/multiple-entrypoints/policy.wasm')
.then(r => r.blob())
.then(b => b.arrayBuffer())

const policy = opa.loadPolicySync(wasm)
this.postMessage(policy.evaluate({}, 'example/one'))
})()
8 changes: 7 additions & 1 deletion test/memory.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { readFileSync } = require("fs");
const { execFileSync } = require("child_process");
const semver = require("semver");
const { loadPolicy } = require("../src/opa.js");
const { loadPolicy, loadPolicySync } = require("../src/opa.js");

describe("growing memory", () => {
const fixturesFolder = "test/fixtures/memory";
Expand Down Expand Up @@ -72,4 +72,10 @@ describe("growing memory", () => {
expect(() => policy.evaluate(input)).not.toThrow();
}
});

it("large input, host and guest grow successfully (synchronous load)", () => {
const policy = loadPolicySync(policyWasm, { initial: 2, maximum: 8 });
const input = "a".repeat(2 * 65536);
expect(() => policy.evaluate(input)).not.toThrow();
});
});

0 comments on commit b3ebf5e

Please sign in to comment.