reflected-ffi
Version:
A remotely reflected Foreign Function Interface
251 lines (233 loc) • 6.68 kB
JavaScript
//@ts-check
import {
FALSE,
TRUE,
UNDEFINED,
NULL,
NUMBER,
UI8,
NAN,
INFINITY,
N_INFINITY,
ZERO,
N_ZERO,
BIGINT,
BIGUINT,
STRING,
SYMBOL,
ARRAY,
BUFFER,
DATE,
ERROR,
MAP,
OBJECT,
REGEXP,
SET,
VIEW,
IMAGE_DATA,
RECURSION,
} from './types.js';
import { ImageData } from './web.js';
import Stack from './array.js';
import { isArray, isView, push } from '../utils/index.js';
import { encoder as textEncoder } from '../utils/text.js';
import { toSymbol } from '../utils/symbol.js';
import { dv, u8a8 } from './views.js';
import { toTag } from '../utils/global.js';
/** @typedef {Map<number, number[]>} Cache */
const { isNaN, isFinite, isInteger } = Number;
const { ownKeys } = Reflect;
const { is } = Object;
/**
* @param {any} input
* @param {number[]|Stack} output
* @param {Cache} cache
* @returns {boolean}
*/
const process = (input, output, cache) => {
const value = cache.get(input);
const unknown = !value;
if (unknown) {
dv.setUint32(0, output.length, true);
cache.set(input, [u8a8[0], u8a8[1], u8a8[2], u8a8[3]]);
}
else
output.push(RECURSION, value[0], value[1], value[2], value[3]);
return unknown;
};
/**
* @param {number[]|Stack} output
* @param {number} type
* @param {number} length
*/
const set = (output, type, length) => {
dv.setUint32(0, length, true);
output.push(type, u8a8[0], u8a8[1], u8a8[2], u8a8[3]);
};
/**
* @param {any} input
* @param {number[]|Stack} output
* @param {Cache} cache
*/
const inflate = (input, output, cache) => {
switch (typeof input) {
case 'number': {
if (input && isFinite(input)) {
if (isInteger(input) && input < 256 && -1 < input)
output.push(UI8, input);
else {
dv.setFloat64(0, input, true);
output.push(NUMBER, u8a8[0], u8a8[1], u8a8[2], u8a8[3], u8a8[4], u8a8[5], u8a8[6], u8a8[7]);
}
}
else if (isNaN(input)) output.push(NAN);
else if (!input) output.push(is(input, 0) ? ZERO : N_ZERO);
else output.push(input < 0 ? N_INFINITY : INFINITY);
break;
}
case 'object': {
switch (true) {
case input === null:
output.push(NULL);
break;
case !process(input, output, cache): break;
case isArray(input): {
const length = input.length;
set(output, ARRAY, length);
for (let i = 0; i < length; i++)
inflate(input[i], output, cache);
break;
}
case isView(input): {
output.push(VIEW);
inflate(toTag(input), output, cache);
input = input.buffer;
if (!process(input, output, cache)) break;
// fallthrough
}
case input instanceof ArrayBuffer: {
const ui8a = new Uint8Array(input);
set(output, BUFFER, ui8a.length);
//@ts-ignore
pushView(output, ui8a);
break;
}
case input instanceof Date:
output.push(DATE);
inflate(input.getTime(), output, cache);
break;
case input instanceof Map: {
set(output, MAP, input.size);
for (const [key, value] of input) {
inflate(key, output, cache);
inflate(value, output, cache);
}
break;
}
case input instanceof Set: {
set(output, SET, input.size);
for (const value of input)
inflate(value, output, cache);
break;
}
case input instanceof Error:
output.push(ERROR);
inflate(input.name, output, cache);
inflate(input.message, output, cache);
inflate(input.stack, output, cache);
break;
/* c8 ignore start */
case input instanceof ImageData:
output.push(IMAGE_DATA);
inflate(input.data, output, cache);
inflate(input.width, output, cache);
inflate(input.height, output, cache);
inflate(input.colorSpace, output, cache);
//@ts-ignore
inflate(input.pixelFormat, output, cache);
break;
/* c8 ignore stop */
case input instanceof RegExp:
output.push(REGEXP);
inflate(input.source, output, cache);
inflate(input.flags, output, cache);
break;
default: {
if ('toJSON' in input) {
const json = input.toJSON();
inflate(json === input ? null : json, output, cache);
}
else {
const keys = ownKeys(input);
const length = keys.length;
set(output, OBJECT, length);
for (let i = 0; i < length; i++) {
const key = keys[i];
inflate(key, output, cache);
inflate(input[key], output, cache);
}
}
break;
}
}
break;
}
case 'string': {
if (process(input, output, cache)) {
const encoded = textEncoder.encode(input);
set(output, STRING, encoded.length);
//@ts-ignore
pushView(output, encoded);
}
break;
}
case 'boolean': {
output.push(input ? TRUE : FALSE);
break;
}
case 'symbol': {
output.push(SYMBOL);
inflate(toSymbol(input), output, cache);
break;
}
case 'bigint': {
let type = BIGINT;
if (9223372036854775807n < input) {
dv.setBigUint64(0, input, true);
type = BIGUINT;
}
else dv.setBigInt64(0, input, true);
output.push(type, u8a8[0], u8a8[1], u8a8[2], u8a8[3], u8a8[4], u8a8[5], u8a8[6], u8a8[7]);
break;
}
// this covers functions too
default: {
output.push(UNDEFINED);
break;
}
}
};
/** @type {typeof push|typeof Stack.push} */
let pushView = push;
/**
* @param {any} value
* @returns {number[]}
*/
export const encode = value => {
const output = [];
pushView = push;
inflate(value, output, new Map);
return output;
};
/**
* @param {{ byteOffset?: number, Array?: typeof Stack }} [options]
* @returns {(value: any, buffer: ArrayBufferLike) => number}
*/
export const encoder = ({ byteOffset = 0, Array = Stack } = {}) => (value, buffer) => {
const output = new Array(buffer, byteOffset);
pushView = Array.push;
inflate(value, output, new Map);
const length = output.length;
output.sync(true);
return length;
};