|
// ############################################################## |
|
// |
|
// STOLEN FROM: https://github.com/open-policy-agent/npm-opa-wasm |
|
// |
|
// ############################################################## |
|
|
|
|
|
function stringDecoder(mem: WebAssembly.Memory): (addr: number) => string { |
|
return function (addr: number): string { |
|
const i8 = new Int8Array(mem.buffer); |
|
let s = ""; |
|
while (i8[addr] !== 0) { |
|
s += String.fromCharCode(i8[addr++]); |
|
} |
|
return s; |
|
}; |
|
} |
|
|
|
function _loadJSON( |
|
wasmInstance: WebAssembly.Instance, |
|
memory: WebAssembly.Memory, |
|
value: any, |
|
): number { |
|
if (value === undefined) { |
|
throw "unable to load undefined value into memory"; |
|
} |
|
|
|
const str = JSON.stringify(value); |
|
// @ts-ignore |
|
const rawAddr = wasmInstance.exports.opa_malloc(str.length); |
|
const buf = new Uint8Array(memory.buffer); |
|
|
|
for (let i = 0; i < str.length; i++) { |
|
let code = str.charCodeAt(i); |
|
|
|
// check for strange encodings |
|
if (code > 126) { |
|
code = 63; // ? |
|
} |
|
buf[rawAddr + i] = code; |
|
} |
|
|
|
// @ts-ignore |
|
const parsedAddr = wasmInstance.exports.opa_json_parse(rawAddr, str.length); |
|
|
|
if (parsedAddr === 0) { |
|
throw "failed to parse json value"; |
|
} |
|
return parsedAddr; |
|
} |
|
|
|
function _dumpJSON( |
|
wasmInstance: any, |
|
memory: WebAssembly.Memory, |
|
addr: number, |
|
): object { |
|
const rawAddr = wasmInstance.exports.opa_json_dump(addr); |
|
const buf = new Uint8Array(memory.buffer); |
|
|
|
let s = ""; |
|
let idx = rawAddr; |
|
|
|
while (buf[idx] !== 0) { |
|
s += String.fromCharCode(buf[idx++]); |
|
} |
|
|
|
return JSON.parse(s); |
|
} |
|
|
|
const builtinFuncs: any = { |
|
count: (arr: Array<any>): number => arr.length, |
|
sum: (arr: Array<any>): number => arr.reduce((a, b) => a + b, 0), |
|
product: (arr: Array<any>): number => |
|
arr.reduce((total, num) => total * num, 1), |
|
max: (arr: Array<any>): number => Math.max(...arr), |
|
min: (arr: Array<any>): number => Math.min(...arr), |
|
sort: (arr: Array<any>): Array<any> => [...arr].sort(), |
|
all: ( |
|
arr: Array<any>, |
|
): boolean => (arr.length === 0 ? true : arr.every((v) => v === true)), |
|
any: ( |
|
arr: Array<any>, |
|
): boolean => (arr.length === 0 ? false : arr.includes(true)), |
|
"array.concat": (arr1: Array<any>, arr2: Array<any>): Array<any> => |
|
arr1.concat(arr2), |
|
"array.slice": ( |
|
arr: Array<any>, |
|
startIndex: number, |
|
stopIndex: number, |
|
): Array<any> => arr.slice(startIndex, stopIndex), |
|
to_number: (x: string): number => Number(x), |
|
|
|
plus: (a: number, b: number): number => a + b, |
|
minus: (a: number, b: number): number => a - b, |
|
mul: (a: number, b: number): number => a * b, |
|
div: (a: number, b: number): number => a / b, |
|
rem: (a: number, b: number): number => a % b, |
|
round: (x: number): number => Math.round(x), |
|
abs: (x: number): number => Math.abs(x), |
|
|
|
re_match: (pattern: string, value: string): boolean => |
|
RegExp(pattern).test(value), |
|
"regex.split": (pattern: string, s: string): Array<string> => |
|
s.split(RegExp(pattern)), |
|
|
|
contains: (s: string, search: string): boolean => s.includes(search), |
|
endswith: (s: string, search: string): boolean => s.endsWith(search), |
|
indexof: (s: string, search: string): number => s.indexOf(search), |
|
lower: (s: string): string => s.toLowerCase(), |
|
replace: (s: string, searchValue: string, newValue: string): string => |
|
s.replace(searchValue, newValue), |
|
split: (s: string, delimiter: string): Array<string> => s.split(delimiter), |
|
sprintf: (s: string, values: string): string => "oh no", |
|
startswith: (s: string, search: string): boolean => s.startsWith(search), |
|
substring: (s: string, start: number, length: number): string => |
|
s.substr(start, length), |
|
concat: (delimiter: string, arr: Array<string>) => arr.join(delimiter), |
|
|
|
is_number: (x: any): boolean => !isNaN(x), |
|
is_string: (x: any): boolean => typeof x == "string", |
|
is_boolean: (x: any): boolean => typeof x == "boolean", |
|
is_array: (x: any): boolean => Array.isArray(x), |
|
is_set: (x: any): boolean => x instanceof Set, |
|
is_object: (x: any): boolean => typeof x == "object", |
|
is_null: (x: any): boolean => x === null, |
|
type_name: (x: any) => typeof x, |
|
}; |
|
|
|
function _builtinCall( |
|
wasmInstance: WebAssembly.Instance, |
|
memory: WebAssembly.Memory, |
|
builtins: { [builtinId: string]: string }, |
|
builtin_id: string, |
|
_1?: any, |
|
_2?: any, |
|
_3?: any, |
|
_4?: any, |
|
): number { |
|
const builtInName = builtins[builtin_id]; |
|
const impl = builtinFuncs[builtInName]; |
|
|
|
if (impl === undefined) { |
|
throw { |
|
message: "not implemented: built-in function " + |
|
builtin_id + |
|
": " + |
|
builtins[builtin_id], |
|
}; |
|
} |
|
|
|
const args = []; |
|
for (const x of [_1, _2, _3, _4]) { |
|
if (!x) { |
|
break; |
|
} |
|
args.push(_dumpJSON(wasmInstance, memory, x)); |
|
} |
|
|
|
const result = impl(...args); |
|
|
|
return _loadJSON(wasmInstance, memory, result); |
|
} |
|
|
|
async function _loadPolicy( |
|
policy_wasm: WebAssembly.Module, |
|
memory: WebAssembly.Memory, |
|
): Promise<WebAssembly.Instance> { |
|
const addr2string = stringDecoder(memory); |
|
let env: any = { |
|
builtins: {}, |
|
}; |
|
|
|
const wasm = await WebAssembly.instantiate(policy_wasm, { |
|
env: { |
|
memory: memory, |
|
opa_abort: function (addr: number) { |
|
throw addr2string(addr); |
|
}, |
|
opa_println: function (addr: number) { |
|
console.log(addr2string(addr)); |
|
}, |
|
opa_builtin0: function (builtin_id: string, ctx: any): number { |
|
return _builtinCall(env.instance, memory, env.builtins, builtin_id); |
|
}, |
|
opa_builtin1: function (builtin_id: string, ctx: any, _1: any): number { |
|
return _builtinCall(env.instance, memory, env.builtins, builtin_id, _1); |
|
}, |
|
opa_builtin2: function ( |
|
builtin_id: string, |
|
ctx: any, |
|
_1: any, |
|
_2: any, |
|
): number { |
|
return _builtinCall( |
|
env.instance, |
|
memory, |
|
env.builtins, |
|
builtin_id, |
|
_1, |
|
_2, |
|
); |
|
}, |
|
opa_builtin3: function ( |
|
builtin_id: string, |
|
ctx: any, |
|
_1: any, |
|
_2: any, |
|
_3: any, |
|
): number { |
|
return _builtinCall( |
|
env.instance, |
|
memory, |
|
env.builtins, |
|
builtin_id, |
|
_1, |
|
_2, |
|
_3, |
|
); |
|
}, |
|
opa_builtin4: function ( |
|
builtin_id: string, |
|
ctx: any, |
|
_1: any, |
|
_2: any, |
|
_3: any, |
|
_4: any, |
|
): number { |
|
return _builtinCall( |
|
env.instance, |
|
memory, |
|
env.builtins, |
|
builtin_id, |
|
_1, |
|
_2, |
|
_3, |
|
_4, |
|
); |
|
}, |
|
}, |
|
}); |
|
|
|
env.instance = wasm; |
|
|
|
const builtins: any = _dumpJSON( |
|
env.instance, |
|
memory, |
|
env.instance.exports.builtins(), |
|
); |
|
|
|
env.builtins = {}; |
|
|
|
for (var key of Object.keys(builtins)) { |
|
env.builtins[builtins[key]] = key; |
|
} |
|
|
|
return wasm; |
|
} |
|
|
|
class LoadedPolicy { |
|
mem: WebAssembly.Memory; |
|
wasmInstance: WebAssembly.Instance; |
|
dataAddr: number; |
|
baseHeapPtr: number; |
|
dataHeapPtr: number; |
|
|
|
constructor(policy: WebAssembly.Instance, memory: WebAssembly.Memory) { |
|
this.mem = memory; |
|
|
|
// Depending on how the wasm was instantiated "policy" might be a |
|
// WebAssembly Instance or be a wrapper around the Module and |
|
// Instance. We only care about the Instance. |
|
this.wasmInstance = policy; |
|
|
|
this.dataAddr = _loadJSON(this.wasmInstance, this.mem, {}); |
|
// @ts-ignore |
|
this.baseHeapPtr = this.wasmInstance.exports.opa_heap_ptr_get(); |
|
this.dataHeapPtr = this.baseHeapPtr; |
|
} |
|
|
|
evaluate(input: any): any { |
|
// Reset the heap pointer before each evaluation |
|
// @ts-ignore |
|
this.wasmInstance.exports.opa_heap_ptr_set(this.dataHeapPtr); |
|
|
|
// Load the input data |
|
const inputAddr: number = _loadJSON(this.wasmInstance, this.mem, input); |
|
|
|
// Setup the evaluation context |
|
// @ts-ignore |
|
const ctxAddr = this.wasmInstance.exports.opa_eval_ctx_new(); |
|
// @ts-ignore |
|
this.wasmInstance.exports.opa_eval_ctx_set_input(ctxAddr, inputAddr); |
|
// @ts-ignore |
|
this.wasmInstance.exports.opa_eval_ctx_set_data(ctxAddr, this.dataAddr); |
|
|
|
// Actually evaluate the policy |
|
// @ts-ignore |
|
this.wasmInstance.exports.eval(ctxAddr); |
|
|
|
// Retrieve the result |
|
// @ts-ignore |
|
const resultAddr = this.wasmInstance.exports.opa_eval_ctx_get_result( |
|
ctxAddr, |
|
); |
|
return _dumpJSON(this.wasmInstance, this.mem, resultAddr); |
|
} |
|
|
|
/** |
|
* Loads data for use in subsequent evaluations. |
|
* @param {object} data |
|
*/ |
|
setData(data: any) { |
|
// @ts-ignore |
|
this.wasmInstance.exports.opa_heap_ptr_set(this.baseHeapPtr); |
|
this.dataAddr = _loadJSON(this.wasmInstance, this.mem, data); |
|
// @ts-ignore |
|
this.dataHeapPtr = this.wasmInstance.exports.opa_heap_ptr_get(); |
|
} |
|
} |
|
|
|
export async function loadPolicy( |
|
regoWasm: WebAssembly.Module, |
|
): Promise<LoadedPolicy> { |
|
const memory = new WebAssembly.Memory({ initial: 10 }); |
|
const policy = await _loadPolicy(regoWasm, memory); |
|
return new LoadedPolicy(policy, memory); |
|
} |