UNPKG

@socketsupply/socket

Version:

A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.

1,978 lines (1,672 loc) 137 kB
/* global Event, ErrorEvent */ /* eslint-disable no-unused-vars */ /** * @module extension * * @example * import extension from 'socket:extension' * const ex = await extension.load('native_extension') * const result = ex.binding.method('argument') */ import { isBufferLike } from './util.js' import { createFile } from './fs/web.js' import application from './application.js' import process from './process.js' import crypto from './crypto.js' import path from './path.js' import ipc from './ipc.js' import fs from './fs/promises.js' /** * @typedef {number} Pointer */ const $loaded = Symbol('loaded') const $stats = Symbol('stats') const $type = Symbol('type') const NULL = 0x0 const EOF = -1 // eslint-disable-next-line const STDIN = 0x0 // STDIN_FILENO const STDOUT = 0x1 // STDOUT_FILENO const STDERR = 0x2 // STDERR_FILNO const EPSILON = 1.1920928955078125e-07 const EXIT_FAILURE = 1 const EXIT_SUCCESS = 0 const RAND_MAX = (0x7fffffff) const PATH_MAX = 4096 const CLOCKS_PER_SEC = 1000 const TIME_UTC = 1 const TIMER_ABSTIME = 1 const CLOCK_REALTIME = 0 const CLOCK_MONOTONIC = 1 const CLOCK_PROCESS_CPUTIME_ID = 2 const CLOCK_THREAD_CPUTIME_ID = 3 const CLOCK_MONOTONIC_RAW = 4 const CLOCK_REALTIME_COARSE = 5 const CLOCK_MONOTONIC_COARSE = 6 const CLOCK_BOOTTIME = 7 const CLOCK_REALTIME_ALARM = 8 const CLOCK_BOOTTIME_ALARM = 9 const CLOCK_SGI_CYCLE = 10 const CLOCK_TAI = 11 const EPERM = 1 const ENOENT = 2 const ESRCH = 3 const EINTR = 4 const EIO = 5 const ENXIO = 6 const E2BIG = 7 const ENOEXEC = 8 const EBADF = 9 const ECHILD = 10 const EAGAIN = 11 const ENOMEM = 12 const EACCES = 13 const EFAULT = 14 const ENOTBLK = 15 const EBUSY = 16 const EEXIST = 17 const EXDEV = 18 const ENODEV = 19 const ENOTDIR = 20 const EISDIR = 21 const EINVAL = 22 const ENFILE = 23 const EMFILE = 24 const ENOTTY = 25 const ETXTBSY = 26 const EFBIG = 27 const ENOSPC = 28 const ESPIPE = 29 const EROFS = 30 const EMLINK = 31 const EPIPE = 32 const EDOM = 33 const ERANGE = 34 const EDEADLK = 35 const ENAMETOOLONG = 36 const ENOLCK = 37 const ENOSYS = 38 const ENOTEMPTY = 39 const ELOOP = 40 const EWOULDBLOCK = EAGAIN const ENOMSG = 42 const EIDRM = 43 const ECHRNG = 44 const EL2NSYNC = 45 const EL3HLT = 46 const EL3RST = 47 const ELNRNG = 48 const EUNATCH = 49 const ENOCSI = 50 const EL2HLT = 51 const EBADE = 52 const EBADR = 53 const EXFULL = 54 const ENOANO = 55 const EBADRQC = 56 const EBADSLT = 57 const EDEADLOCK = EDEADLK const EBFONT = 59 const ENOSTR = 60 const ENODATA = 61 const ETIME = 62 const ENOSR = 63 const ENONET = 64 const ENOPKG = 65 const EREMOTE = 66 const ENOLINK = 67 const EADV = 68 const ESRMNT = 69 const ECOMM = 70 const EPROTO = 71 const EMULTIHOP = 72 const EDOTDOT = 73 const EBADMSG = 74 const EOVERFLOW = 75 const ENOTUNIQ = 76 const EBADFD = 77 const EREMCHG = 78 const ELIBACC = 79 const ELIBBAD = 80 const ELIBSCN = 81 const ELIBMAX = 82 const ELIBEXEC = 83 const EILSEQ = 84 const ERESTART = 85 const ESTRPIPE = 86 const EUSERS = 87 const ENOTSOCK = 88 const EDESTADDRREQ = 89 const EMSGSIZE = 90 const EPROTOTYPE = 91 const ENOPROTOOPT = 92 const EPROTONOSUPPORT = 93 const ESOCKTNOSUPPORT = 94 const EOPNOTSUPP = 95 const ENOTSUP = EOPNOTSUPP const EPFNOSUPPORT = 96 const EAFNOSUPPORT = 97 const EADDRINUSE = 98 const EADDRNOTAVAIL = 99 const ENETDOWN = 100 const ENETUNREACH = 101 const ENETRESET = 102 const ECONNABORTED = 103 const ECONNRESET = 104 const ENOBUFS = 105 const EISCONN = 106 const ENOTCONN = 107 const ESHUTDOWN = 108 const ETOOMANYREFS = 109 const ETIMEDOUT = 110 const ECONNREFUSED = 111 const EHOSTDOWN = 112 const EHOSTUNREACH = 113 const EALREADY = 114 const EINPROGRESS = 115 const ESTALE = 116 const EUCLEAN = 117 const ENOTNAM = 118 const ENAVAIL = 119 const EISNAM = 120 const EREMOTEIO = 121 const EDQUOT = 122 const ENOMEDIUM = 123 const EMEDIUMTYPE = 124 const ECANCELED = 125 const ENOKEY = 126 const EKEYEXPIRED = 127 const EKEYREVOKED = 128 const EKEYREJECTED = 129 const EOWNERDEAD = 130 const ENOTRECOVERABLE = 131 const ERFKILL = 132 const EHWPOISON = 133 const SAPI_JSON_TYPE_EMPTY = -1 const SAPI_JSON_TYPE_ANY = 0 const SAPI_JSON_TYPE_NULL = 1 const SAPI_JSON_TYPE_OBJECT = 2 const SAPI_JSON_TYPE_ARRAY = 3 const SAPI_JSON_TYPE_BOOLEAN = 4 const SAPI_JSON_TYPE_NUMBER = 5 const SAPI_JSON_TYPE_STRING = 6 const SAPI_JSON_TYPE_RAW = 7 const REG_EXTENDED = 0x1 const REG_ICASE = 0x2 const REG_NOSUB = 0x4 const REG_NEWLINE = 0x8 const REG_NOMATCH = 1 const REG_BADPAT = 2 const REG_ECOLLATE = 3 const REG_ECTYPE = 4 const REG_EESCAPE = 5 const REG_ESUBREG = 6 const REG_EBRACK = 7 const REG_EPAREN = 8 const REG_EBRACE = 9 const REG_BADBR = 10 const REG_ERANGE = 11 const REG_ESPACE = 12 const REG_BADRPT = 13 const REG_ENOSYS = 14 const REG_NOTBOL = 0x1 const REG_NOTEOL = 0x2 const errorMessages = { 0: 'Undefined error: 0', [EPERM]: 'Operation not permitted', [ENOENT]: 'No such file or directory', [ESRCH]: 'No such process', [EINTR]: 'Interrupted system call', [EIO]: 'Input/output error', [ENXIO]: 'Device not configured', [E2BIG]: 'Argument list too long', [ENOEXEC]: 'Exec format error', [EBADF]: 'Bad file descriptor', [ECHILD]: 'No child processes', [EAGAIN]: 'Try again', [ENOMEM]: 'Out of memory', [EACCES]: 'Permission denied', [EFAULT]: 'Bad address', [ENOTBLK]: 'Block device required', [EBUSY]: 'Device or resource busy', [EEXIST]: 'File exists', [EXDEV]: 'Cross-device link', [ENODEV]: 'No such device', [ENOTDIR]: 'Not a directory', [EISDIR]: 'Is a directory', [EINVAL]: 'Invalid argument', [ENFILE]: 'File table overflow', [EMFILE]: 'Too many open files', [ENOTTY]: 'Not a typewriter', [ETXTBSY]: 'Text file busy', [EFBIG]: 'File too large', [ENOSPC]: 'No space left on device', [ESPIPE]: 'Illegal seek', [EROFS]: 'Read-only file system ', [EMLINK]: 'Too many links', [EPIPE]: 'Broken pipe', [EDOM]: ' Numerical argument out of domain', [ERANGE]: 'umerical result out of range', [EDEADLK]: 'Resource deadlock avoided', [ENAMETOOLONG]: 'File name too long', [ENOLCK]: 'No locks available', [ENOSYS]: 'Function not implemented', [ENOTEMPTY]: 'Directory not empty', [ELOOP]: 'Too many levels of symbolic links', [EWOULDBLOCK]: 'Try again', [ENOMSG]: 'No message of desired type', [EIDRM]: 'Identifier removed', [ECHRNG]: 'Character range error', [EL2NSYNC]: 'Level 2 not synchronized', [EL3HLT]: 'Level 3 halted', [EL3RST]: 'EL3RST', [ELNRNG]: 'ELNRNG', [EUNATCH]: 'No such device', [ENOCSI]: 'No CSI structure available', [EL2HLT]: 'Level 2 halted', [EBADE]: 'Invalid exchange', [EBADR]: 'Invalid request descriptor', [EXFULL]: 'Exchange full', [ENOANO]: 'No anode', [EBADRQC]: 'Invalid request code', [EBADSLT]: 'Invalid slot', [EDEADLOCK]: 'Resource deadlock avoided', [EBFONT]: 'Bad font file format', [ENOSTR]: 'Device not a stream', [ENODATA]: 'No data available', [ETIME]: 'Timer expired', [ENOSR]: 'Out of streams resources', [ENONET]: 'Machine is not on the network', [ENOPKG]: 'Package not installed', [EREMOTE]: 'Object is remote', [ENOLINK]: 'Link has been severed', [EADV]: 'Advertise error', [ESRMNT]: 'Srmount error', [ECOMM]: 'Communication error on send', [EPROTO]: 'Protocol error', [EMULTIHOP]: 'Multihop attempted', [EDOTDOT]: 'RFS specific error', [EBADMSG]: 'Not a data message', [EOVERFLOW]: 'Value too large for defined data type', [ENOTUNIQ]: 'Name not unique on network', [EBADFD]: 'File descriptor in bad state', [EREMCHG]: 'Remote address changed', [ELIBACC]: 'Can not access a needed shared library', [ELIBBAD]: 'Accessing a corrupted shared library', [ELIBSCN]: '.lib section in a.out corrupted', [ELIBMAX]: 'Attempting to link in too many shared libraries', [ELIBEXEC]: 'Cannot exec a shared library directly', [EILSEQ]: 'Illegal byte sequence', [ERESTART]: 'Interrupted system call should be restarted', [ESTRPIPE]: 'Streams pipe error', [EUSERS]: 'Too many users', [ENOTSOCK]: 'Socket operation on non-socket', [EDESTADDRREQ]: 'Destination address required', [EMSGSIZE]: 'Message too long', [EPROTOTYPE]: 'Protocol wrong type for socket', [ENOPROTOOPT]: 'Protocol not available', [EPROTONOSUPPORT]: 'Protocol not supported', [ESOCKTNOSUPPORT]: 'Socket type not supported', [EOPNOTSUPP]: 'Operation not supported', [ENOTSUP]: 'Operation not supported', [EPFNOSUPPORT]: 'Protocol family not supported', [EAFNOSUPPORT]: 'Address family not supported by protocol', [EADDRINUSE]: 'Address already in use', [EADDRNOTAVAIL]: 'Cannot assign requested address', [ENETDOWN]: 'Network is down', [ENETUNREACH]: 'Network is unreachable', [ENETRESET]: 'Network dropped connection because of reset', [ECONNABORTED]: 'Software caused connection abort', [ECONNRESET]: 'Connection reset by peer', [ENOBUFS]: 'No buffer space available', [EISCONN]: 'Transport endpoint is already connected', [ENOTCONN]: 'Transport endpoint is not connected', [ESHUTDOWN]: 'Cannot send after transport endpoint shutdown', [ETOOMANYREFS]: 'Too many references: cannot splice', [ETIMEDOUT]: 'Connection timed out', [ECONNREFUSED]: 'Connection refused', [EHOSTDOWN]: 'Host is down', [EHOSTUNREACH]: 'No route to host', [EALREADY]: 'Operation already in progress', [EINPROGRESS]: 'Operation now in progress', [ESTALE]: 'Stale NFS file handle', [EUCLEAN]: 'Structure needs cleaning', [ENOTNAM]: 'Not a XENIX named type file', [ENAVAIL]: 'No XENIX semaphores available', [EISNAM]: 'Is a named type file', [EREMOTEIO]: 'Remote I/O error', [EDQUOT]: 'Quota exceeded', [ENOMEDIUM]: 'No medium found', [EMEDIUMTYPE]: 'Wrong medium type', [ECANCELED]: 'Operation canceled', [ENOKEY]: 'Required key not available', [EKEYEXPIRED]: 'Key has expired', [EKEYREVOKED]: 'Key has been revoked', [EKEYREJECTED]: 'Key was rejected by service', [EOWNERDEAD]: 'Owner died', [ENOTRECOVERABLE]: 'State not recoverable', [ERFKILL]: 'Operation not possible due to RF-kill', [EHWPOISON]: 'Memory page has hardware error' } class WebAssemblyExtensionRuntimeObject { static get pointerSize () { return 4 } adapter = null constructor (adapter) { this.adapter = adapter } } class WebAssemblyExtensionRuntimeBuffer extends WebAssemblyExtensionRuntimeObject { context = null pointer = NULL #buffer = new Uint8Array(0) #bufferPointer = NULL constructor (adapter, context) { super(adapter) this.context = context this.pointer = context.createExternalReferenceValue(this) } get buffer () { return this.#buffer } get bufferPointer () { return this.#bufferPointer } set (pointer, size) { this.#buffer = new Uint8Array(size) this.#buffer.fill(0) this.#buffer.set(this.adapter.get(pointer, size)) this.#bufferPointer = pointer } } class WebAssemblyExtensionRuntimeJSON extends WebAssemblyExtensionRuntimeObject { context = null pointer = NULL #value = null constructor (adapter, context, value) { super(adapter) this.context = context this.pointer = context.createExternalReferenceValue(this) this.value = value } set value (value) { this.#value = value } get value () { return this.#value } } class WebAssemblyExtensionRuntimeIPCResultJSON extends WebAssemblyExtensionRuntimeJSON { #data = null #err = null constructor (adapter, context, value) { super(adapter, context, value) this.#data = new WebAssemblyExtensionRuntimeJSON(adapter, context, this.value?.data ?? null) this.#err = new WebAssemblyExtensionRuntimeJSON(adapter, context, this.value?.err ?? null) } set value (value) { if (value?.data) { this.#data.value = value.data } if (value?.err) { this.#err.value = value.err } super.value = value } get value () { return super.value } get data () { return this.#data } get err () { return this.#err } } class WebAssemblyExtensionRuntimeIPCResultHeaders extends WebAssemblyExtensionRuntimeObject { #headers = new ipc.Headers() #pointerMap = new Map() context = null pointer = NULL constructor (adapter, context) { super(adapter) this.context = context this.pointer = context.createExternalReferenceValue(this) } get value () { return this.#headers } get pointerMap () { return this.#pointerMap } set (name, value) { this.#headers.set(name, value) this.#pointerMap.set(name, this.context.createExternalReferenceValue(value)) } get (name) { return this.#headers.get(name) } getValuePointer (name) { return this.#pointerMap.get(name) } } class WebAssemblyExtensionRuntimeIPCMessage extends WebAssemblyExtensionRuntimeObject { context = null pointer = NULL #id = null #name = null #seq = null #uri = null #value = null #values = {} #bytes = null #message = null constructor (adapter, context, url, ...args) { super(adapter) this.context = context this.pointer = context.createExternalReferenceValue(this) this.#message = ipc.Message.from(url, ...args) this.#id = new WebAssemblyExtensionRuntimeJSON(adapter, context, this.#message.id) this.#name = new WebAssemblyExtensionRuntimeJSON(adapter, context, this.#message.name) this.#uri = new WebAssemblyExtensionRuntimeJSON(adapter, context, this.#message.toString()) this.#seq = new WebAssemblyExtensionRuntimeJSON(adapter, context, this.#message.seq) this.#value = new WebAssemblyExtensionRuntimeJSON(adapter, context, this.#message.value) this.#bytes = new WebAssemblyExtensionRuntimeBuffer(adapter, context, this.#message.bytes) for (const key of this.#message.searchParams.keys()) { const value = this.#message.get(key) this.#values[key] = new WebAssemblyExtensionRuntimeJSON(adapter, context, value) } } get id () { return this.#id } get name () { return this.#name } get uri () { return this.#uri } get seq () { return this.#seq } get index () { return this.#message.index } get value () { return this.#value } get bytes () { return this.#bytes } get (key) { return this.#values[key] } } class WebAssemblyExtensionRuntimeIPCResult extends WebAssemblyExtensionRuntimeObject { #message = null #bytes = null #headers = null #json = null context = null pointer = NULL result = null source = null seq = null id = null constructor (adapter, context, message, existingResult = null) { super(adapter) this.#json = new WebAssemblyExtensionRuntimeIPCResultJSON(adapter, context, {}) this.#bytes = new WebAssemblyExtensionRuntimeBuffer(adapter, context) this.#headers = new WebAssemblyExtensionRuntimeIPCResultHeaders(adapter, context) this.pointer = context.createExternalReferenceValue(this) this.context = context this.message = message this.source = message.name if (this.result && existingResult) { this.id = existingResult.id this.seq = existingResult.seq this.err = existingResult.err this.data = existingResult.data this.source = existingResult.source for (const entry of existingResult.pointerMap.entries()) { this.#headers.pointerMap.set(entry[0], entry[1]) } for (const entry of existingResult.headers.entries()) { this.#headers.value.set(entry[0], entry[1]) } } } set message (message) { this.#message = message this.seq = message.seq this.source = message.name this.result = ipc.Result.from( { id: message.id }, null, message.name, message.headers ) } get message () { return this.#message } get headers () { return this.#headers } get bytes () { return this.#bytes } set data (data) { this.#json.data.value = data ?? null } get data () { return this.#json.data } // eslint-disable-next-line set err (err) { this.#json.err.value = err ?? null } get err () { return this.#json.err } set json (json) { this.#json.value = json this.source = json?.source ?? this.source this.data = json?.data ?? this.data this.err = json?.err ?? this.err this.id = json?.id ?? this.id } get json () { return this.#json } toJSON () { return this.#json.value } reply () { if (!this.context) { this.result = null return false } const callback = this.context.internal?.callback this.result.headers = this.headers.value this.result.source = this.source.value this.result.err = this.err.value ?? null this.result.id = this.id || '' if (this.bytes.buffer.byteLength > 0) { this.result.data = this.bytes.buffer } else { this.result.data = this.data.value ?? this.json.value } if (typeof callback === 'function') { callback(this.result) } this.context.release() this.result = null this.context = null return typeof callback === 'function' } } class WebAssemblyExtensionRuntimeIPCRouter extends WebAssemblyExtensionRuntimeObject { routes = new Map() pointer = NULL constructor (adapter) { super(adapter) this.pointer = adapter.createExternalReferenceValue(this) } map (route, callback) { this.routes.set(route, callback) } unmap (route) { this.routes.delete(route) } invoke (context, url, size, bytes, callback) { if (typeof size === 'function') { callback = size bytes = null size = 0 } if (!url.startsWith('ipc://')) { url = `ipc://${url}` } const { adapter } = this const { name } = ipc.Message.from(url) if (this.routes.has(name)) { const routeContext = new WebAssemblyExtensionRuntimeContext(adapter, { context }) const message = new WebAssemblyExtensionRuntimeIPCMessage( adapter, routeContext, url, null, bytes ) routeContext.internal.callback = callback const route = this.routes.get(name) route(routeContext, message, this) return true } return false } } class WebAssemblyExtensionRuntimePolicy extends WebAssemblyExtensionRuntimeObject { name = null allowed = false constructor (adapter, name, allowed) { super(adapter) this.name = name this.allowed = Boolean(allowed) } } class WebAssemblyExtensionRuntimeContext extends WebAssemblyExtensionRuntimeObject { // internal pointers data = NULL pointer = NULL pool = [] error = null router = null internal = { callback: null } config = Object.create(application.config) context = null retained = false policies = new Map() retainedCount = 0 constructor (adapter, options) { super(adapter) this.context = options?.context ?? null const policies = options?.policies ?? options?.context?.policies ?? null if (Array.isArray(policies)) { for (const policy of policies) { this.setPolicy(policy, true) } } this.router = this.context?.router ?? new WebAssemblyExtensionRuntimeIPCRouter(adapter) this.config = this.context?.config ? Object.create(this.context.config) : this.config this.retained = Boolean(options?.retained) this.policies = this.context?.policies ?? this.policies this.pointer = this.context ? this.context.alloc(WebAssemblyExtensionRuntimeObject.pointerSize) : this.adapter.heap.alloc(WebAssemblyExtensionRuntimeObject.pointerSize) this.adapter.setExternalReferenceValue(this.pointer, this) } alloc (size) { const pointer = this.adapter.heap.alloc(size) if (pointer) { this.pool.push(pointer) } return pointer } free (pointer) { const index = this.pool.indexOf(pointer) if (index > -1) { this.pool.splice(index, 1) this.adapter.heap.free(pointer) } } createExternalReferenceValue (value) { const pointer = this.adapter.createExternalReferenceValue(value) if (pointer) { this.pool.push(pointer) } return pointer } retain () { this.retained = true this.retainedCount++ } release () { if (this.retainedCount === 0) { return false } if (--this.retainedCount === 0) { for (const pointer of this.pool) { const object = this.adapter.getExternalReferenceValue(pointer) if (typeof object?.release === 'function') { object.release() } this.adapter.heap.free(pointer) } this.adapter.heap.free(this.pointer) this.pool.splice(0, this.pool.length) this.pointer = NULL this.retained = false this.context = null return true } return false } isAllowed (name) { const names = name.split(',') for (const name of names) { const parts = name.trim().split('_') let key = '' for (const part of parts) { key += part if (this.hasPolicy(key) && this.getPolicy(key).allowed) { return true } key += '_' } } return false } setPolicy (name, allowed) { if (this.hasPolicy(name)) { const policy = this.getPolicy(name) policy.name = name policy.allowed = Boolean(allowed) } else { this.policies.set(name, new WebAssemblyExtensionRuntimePolicy( this.adapter, name, allowed )) } } getPolicy (name) { return this.policies.get(name) ?? null } hasPolicy (name) { return this.policies.has(name) } } /** * A constructed `Response` suitable for WASM binaries. * @ignore */ class WebAssemblyResponse extends Response { constructor (stream) { super(stream, { headers: { 'content-type': 'application/wasm' } }) } } class WebAssemblyExtensionExternalReference { adapter = null pointer = NULL value = null constructor (adapter, value, pointer) { // box primitive values for proper equality comparisons if (typeof value === 'string') { // eslint-disable-next-line value = new String(value) } else if (typeof value === 'number') { // eslint-disable-next-line value = new Number(value) } else if (typeof value === 'boolean') { // eslint-disable-next-line value = new Boolean(value) } this.adapter = adapter this.pointer = pointer || adapter.heap.alloc(4) this.value = value } valueOf () { return this.value?.valueOf?.() ?? undefined } toString () { return this.value?.toString?.() ?? '' } } class WebAssemblyExtensionMemoryBlock { memory = null start = 0 end = 0 constructor (memory, start = null, end = null) { this.memory = memory this.start = start || 0 this.end = end || memory.byteLength } } class WebAssemblyExtensionMemory { adapter = null offset = 0 current = [] limits = { min: 0, max: 0 } freeList = [] constructor (adapter, min, max) { this.adapter = adapter this.limits.min = min this.limits.max = max this.offset = min this.freeList.push(new WebAssemblyExtensionMemoryBlock(this)) } get buffer () { return this.adapter.buffer } get byteLength () { return this.buffer.byteLength } get pointer () { return this.offset } get (pointer, size = -1) { if (size > -1) { return this.buffer.subarray(pointer, pointer + size) } else { return this.buffer.subarray(pointer) } } set (pointer, byteOrBytes, size) { if (!pointer || !size) { return false } if (typeof byteOrBytes === 'number') { const byte = byteOrBytes byteOrBytes = new Uint8Array(size) byteOrBytes.fill(byte) } this.buffer.set(byteOrBytes.slice(0, size), pointer) return true } push (value) { let pointer = this.offset if (value === null) { value = 0 } if (typeof value === 'string') { value = this.adapter.textEncoder.encode(value) } else if (isBufferLike(value)) { value = new Uint8Array(value) } if (!isBufferLike(value) && typeof value !== 'number') { throw new TypeError( 'WebAssemblyExtensionMemory: ' + 'Expected value to be TypedArray or a number: ' + `Got ${typeof value}` ) } let byteLength = 0 const isFloat = typeof value === 'number' && parseInt(String(value)) !== value if (isBufferLike(value)) { byteLength = value.byteLength pointer = this.alloc(byteLength) } else { const zeroes = Math.clz32(value) if (zeroes <= 8) { byteLength = 4 } else if (zeroes <= 16) { byteLength = 2 } else if (zeroes <= 32) { byteLength = 1 } } this.current.push(byteLength) this.offset += byteLength if (isBufferLike(value)) { this.adapter.set(pointer, value) } else if (byteLength === 4) { if (isFloat) { this.adapter.setFloat32(pointer, value) } else { this.adapter.setInt32(pointer, value) } } else if (byteLength === 2) { if (isFloat) { this.adapter.setFloat16(pointer, value) } else { this.adapter.setInt16(pointer, value) } } else if (byteLength === 1) { if (isFloat) { this.adapter.setFloat8(pointer, value) } else { this.adapter.setInt8(pointer, value) } } return pointer } pop () { if (this.current.length === 0) { return 0 } const byteLength = this.current.pop() const pointer = this.offset - byteLength this.offset -= byteLength return pointer } restore (offset) { const pointers = [] while (this.offset > offset) { pointers.push(this.pop()) } return pointers } alloc (size) { if (!size) { return NULL } // find a free block that is large enough for (let i = 0; i < this.freeList.length; ++i) { const block = this.freeList[i] if (block.end - block.start >= size) { // allocate memory from the free block const allocatedBlock = new WebAssemblyExtensionMemoryBlock( this, block.start, block.start + size ) // update the free block (if any remaining) if (block.end - allocatedBlock.end > 0) { block.start = allocatedBlock.end } else { // remove the block from the free list if no remaining space this.freeList.splice(i, 1) } this.offset = allocatedBlock.end const pointer = this.limits.min + allocatedBlock.start return pointer } } return NULL } free (pointer) { if (!pointer) { return } if (this.adapter.externalReferences.has(pointer)) { this.adapter.externalReferences.delete(pointer) } pointer = pointer - this.limits.min // find the block to free and merge adjacent free blocks for (let i = 0; i < this.freeList.length; ++i) { const block = this.freeList[i] if (pointer >= block.start && pointer < block.end) { // add the freed block back to the free list this.freeList.splice(i, 0, new WebAssemblyExtensionMemoryBlock( this, pointer, pointer + (block.end - block.start) )) // merge adjacent free blocks if (i < this.freeList.length - 1 && this.freeList[i + 1].start === block.end) { this.freeList[i].end = this.freeList[i + 1].end this.freeList.splice(i + 1, 1) } if (i > 0 && this.freeList[i - 1].end === block.start) { this.freeList[i - 1].end = block.end this.freeList.splice(i, 1) } this.offset = this.freeList[this.freeList.length - 1].start return } } } } class WebAssemblyExtensionStack extends WebAssemblyExtensionMemory { constructor (adapter) { super( adapter, adapter.instance.exports.__stack_low.value, adapter.instance.exports.__stack_high.value ) } } class WebAssemblyExtensionHeap extends WebAssemblyExtensionMemory { constructor (adapter) { super( adapter, adapter.instance.exports.__heap_base.value, adapter.instance.exports.__heap_end.value ) } realloc (pointer, size) { // if the pointer is NULL, it's equivalent to `alloc()` if (pointer === NULL) { return this.alloc(size) } // find the block corresponding to the given pointer for (let i = 0; i < this.freeList.length; ++i) { const block = this.freeList[i] if (pointer >= block.start && pointer < block.end) { const currentSize = block.end - block.start // if the new size is smaller or equal, return the existing pointer if (size <= currentSize) { return pointer } // try to extend the current block if there is enough space if ( i < this.freeList.length - 1 && this.freeList[i + 1].start - block.end >= size - currentSize ) { block.end += size - currentSize return pointer } // if extension is not possible, allocate a new block and copy the data const newPointer = this.alloc(size) const sourceArray = new Uint8Array(this.buffer.buffer, block.start, currentSize) const destinationArray = new Uint8Array(this.buffer.buffer, newPointer, currentSize) destinationArray.set(sourceArray) // free the original block this.free(pointer) return newPointer } } // return NULL if the pointer is not found (not allocated) return NULL } calloc (count, size) { const totalSize = count * size const pointer = this.alloc(totalSize) if (pointer === NULL) { // allocation failed return NULL } // initialize the allocated memory to zero const array = new Uint8Array(this.buffer.buffer, pointer, totalSize) array.fill(0) return pointer } } class WebAssemblyExtensionIndirectFunctionTable { constructor (adapter, options = null) { this.adapter = adapter this.table = ( options?.table ?? adapter.instance?.exports?.__indirect_function_table ?? new WebAssembly.Table({ initial: 0, element: 'anyfunc' }) ) } call (index, ...args) { if (this.table.length === 0 || index >= this.table.length) { return null } const callable = this.table.get(index) const values = [] const mark = this.adapter.stack.pointer for (const arg of args) { if (typeof arg === 'number') { values.push(arg) } else { values.push(this.adapter.stack.push(arg)) } } const value = callable(...values) this.adapter.stack.restore(mark) return value } } /** * An adapter for reading and writing various values from a WebAssembly instance's * memory buffer. * @ignore */ class WebAssemblyExtensionAdapter { view = null heap = null table = null stack = null buffer = null module = null memory = null context = null policies = [] externalReferences = new Map() // mapping pointer addresses to javascript objects instance = null exitStatus = null textDecoder = new TextDecoder() textEncoder = new TextEncoder() errorMessagePointers = {} indirectFunctionTable = null constructor ({ instance, module, table, memory, policies }) { this.view = new DataView(memory.buffer) this.table = table this.buffer = new Uint8Array(memory.buffer) this.module = module this.memory = memory this.policies = policies || [] this.instance = instance this.indirectFunctionTable = new WebAssemblyExtensionIndirectFunctionTable(this) } get globalBaseOffset () { return this.instance.exports.__global_base.value } destroy () { this.view = null this.table = null this.buffer = null this.module = null this.memory = null this.instance = null this.textDecoder = null this.textEncoder = null this.indirectFunctionTable = null this.stack = null this.heap = null this.context = null } init () { this.stack = new WebAssemblyExtensionStack(this) this.heap = new WebAssemblyExtensionHeap(this) this.instance.exports.__wasm_call_ctors() const offset = this.instance.exports.__sapi_extension_initializer() this.context = new WebAssemblyExtensionRuntimeContext(this, { policies: this.policies || [] }) // preallocate error message pointertr for (const errorCode in errorMessages) { const errorMessage = errorMessages[errorCode] const pointer = this.heap.alloc(errorMessage.length) this.errorMessagePointers[errorCode] = pointer this.setString(pointer, errorMessage) } return Boolean(this.indirectFunctionTable.call(offset, this.context.pointer)) } get (pointer, size = -1) { if (!pointer) { return null } if (size > -1) { return this.buffer.subarray(pointer, pointer + size) } return this.buffer.subarray(pointer) } set (pointer, value) { if (pointer) { if (typeof value === 'string') { value = this.textEncoder.encode(value) } this.buffer.set(value, pointer) } } createExternalReferenceValue (value) { const pointer = this.heap.alloc(4) this.setExternalReferenceValue(pointer, value) return pointer } getExternalReferenceValue (pointer) { const ref = this.externalReferences.get(pointer) if (ref) { if (ref.value === null) { return NULL } if (typeof ref.value === 'object') { return ref.value.valueOf() } return ref.value } } setExternalReferenceValue (pointer, value) { return this.externalReferences.set( pointer, new WebAssemblyExtensionExternalReference(this, value, pointer) ) } removeExternalReferenceValue (pointer) { this.externalReferences.delete(pointer) } getExternalReferencePointer (value) { for (const ref of this.externalReferences.values()) { if (ref.value === value) { return ref.pointer } } return NULL } getFloat32 (pointer) { return pointer ? this.view.getFloat32(pointer, true) : 0 } setFloat32 (pointer, value) { return pointer ? (this.view.setFloat32(pointer, value, true), true) : false } getFloat64 (pointer) { return pointer ? this.view.getFloat64(pointer, true) : 0 } setFloat64 (pointer, value) { return pointer ? (this.view.setFloat64(pointer, value, true), true) : false } getInt8 (pointer) { return pointer ? this.view.getInt8(pointer, true) : 0 } setInt8 (pointer, value) { return pointer ? (this.view.setInt8(pointer, value, true), true) : false } getInt16 (pointer) { return pointer ? this.view.getInt16(pointer, true) : 0 } setInt16 (pointer, value) { return pointer ? (this.view.setInt16(pointer, value, true), true) : false } getInt32 (pointer) { return pointer ? this.view.getInt32(pointer, true) : 0 } setInt32 (pointer, value) { return pointer ? (this.view.setInt32(pointer, value, true), true) : false } getUint8 (pointer) { return pointer ? this.view.getUint8(pointer, true) : 0 } setUint8 (pointer, value) { return pointer ? (this.view.setUint8(pointer, value, true), true) : false } getUint16 (pointer) { return pointer ? this.view.getUint16(pointer, true) : 0 } setUint16 (pointer, value) { return pointer ? (this.view.setUint16(pointer, value, true), true) : false } getUint32 (pointer) { return pointer ? this.view.getUint32(pointer, true) : 0 } setUint32 (pointer, value) { return pointer ? (this.view.setUint32(pointer, value, true), true) : false } getString (pointer, buffer, size) { if (!pointer) { return null } if (typeof buffer === 'number') { size = buffer buffer = null } const start = buffer ? buffer.slice(pointer) : this.buffer.subarray(pointer) const end = size || start.indexOf(0) // NULL byte if (end === -1) { return this.textDecoder.decode(start) } return this.textDecoder.decode(start.slice(0, end)) } setString (pointer, string, buffer = null) { if (pointer) { const encoded = this.textEncoder.encode(string) ;(buffer || this.buffer).set(encoded, pointer) return true } return false } } /** * A container for a native WebAssembly extension info. * @ignore */ class WebAssemblyExtensionInfo { adapter = null constructor (adapter) { this.adapter = adapter } get instance () { return this.adapter.instance } get abi () { return this.adapter.instance.exports.__sapi_extension_abi() } get name () { return this.adapter.getString(this.instance.exports.__sapi_extension_name()) } get version () { return this.adapter.getString(this.instance.exports.__sapi_extension_version()) } get description () { return this.adapter.getString(this.instance.exports.__sapi_extension_description()) } } function createWebAssemblyExtensionBinding (extension, adapter) { return new Proxy(adapter, { get (_, key) { if (typeof adapter.instance.exports[key] === 'function') { return adapter.instance.exports[key].bind(adapter.instance.exports) } if (!adapter.context.router.routes.has(key)) { key = `${extension.name}.${String(key)}` } if (adapter.context.router.routes.has(key)) { return (options, ...args) => { return new Promise((resolve, reject) => { let params = null if (typeof options === 'object') { params = String(new URLSearchParams(options)) } else if (typeof options === 'string') { params = options } const rest = Array.from(args).concat([resolve]) const url = `ipc://${key}?${params}` try { adapter.context.router.invoke( adapter.context, url, ...rest ) } catch (err) { reject(err) } }) } } return null } }) } function variadicFormattedestinationPointerringFromPointers ( env, formatPointer, variadicArguments = 0x0, encoded = true ) { const format = env.adapter.getString(formatPointer) if (!variadicArguments) { if (encoded) { return env.adapter.textEncoder.encode(format) } return format } const view = new DataView(env.adapter.buffer.buffer, variadicArguments) let index = 0 const regex = /%(|l|ll|j|t|z|\.\*)+(l|d|f|i|n|o|p|s|S|u|x|X|%)+/gi const output = format.replace(regex, (x) => { if (x === '%%') { return '%' } switch (x.toLowerCase()) { case '%.*lu': case '%.*llu': case '%.*ls': case '%.*lls': case '%.*u': case '%.*s': { const size = view.getInt32((index++) * 4, true) const pointer = view.getInt32((index++) * 4, true) const string = ( env.adapter.getString(pointer) || env.adapter.getExternalReferenceValue(pointer)?.value ) if (!string || typeof string !== 'string') { return '(null)' } return string.slice(0, size) } case '%s': { const pointer = view.getInt32((index++) * 4, true) const string = ( env.adapter.getString(pointer) || env.adapter.getExternalReferenceValue(pointer)?.value ) if (!string || typeof string !== 'string') { return '(null)' } return string } case '%zu': case '%u': case '%llu': case '%lu': { return view.getUint32((index) * 4, true) } case '%lld': case '%ld': case '%i': case '%x': case '%d': { return view.getInt32((index++) * 4, true) } case '%lp': case '%p': { return `0x${view.getUint32((index++) * 4, true).toString(16)}` } case '%f': case '%lf': case '%llf': { return view.getFloat64((index++) * 8, true) } } return x }) if (encoded) { return env.adapter.textEncoder.encode(output) } return output } function createWebAssemblyExtensionImports (env) { const imports = { env: { memoryBase: 5 * 1024 * 1024, tableBase: 0, memory: env.memory, table: env.table || new WebAssembly.Table({ initial: 0, element: 'anyfunc' }) } } let atExitCallbackPointer = 0x0 let srandSeed = 1 // internal pointers let basenamePointer = 0 let dirnamePointer = 0 let errnoPointer = 0 const toBeFlushed = { stdout: [], stderr: [] } // <assert.h> Object.assign(imports.env, { __assert_fail ( conditionStringPointer, filenameStringPointer, lineNumber, functionStringPointer ) { console.error( 'Assertion failed: (%s), function %s, file %s, line %d.', env.adapter.getString(conditionStringPointer), env.adapter.getString(functionStringPointer), env.adapter.getString(filenameStringPointer), lineNumber ) imports.env.abort() } }) // <ctype.h> Object.assign(imports.env, { isalnum (char) { return imports.env.isalpha(char) || imports.env.isdigit(char) }, isalpha (char) { return char === EOF || imports.env.islower(char) || imports.env.isupper(char) }, isascii (char) { return char >= 0 && char <= parseInt('0177', 8) }, isblank (char) { return /\s/.test(String.fromCharCode(char)) }, iscntrl (char) { const codes = [ '000', '001', '002', '003', '004', '005', '006', '007', '010', '011', '012', '013', '014', '015', '016', '017', '020', '021', '022', '023', '024', '025', '026', '027', '030', '031', '032', '033', '034', '035', '036', '037', '177' ].map((c) => parseInt(c, 8)) return codes.includes(char) }, isdigit (char) { return !Number.isNaN(parseInt(char, 10)) }, isgraph (char) { char = String.fromCharCode(char) return char.length === 1 && char >= '!' && char <= '~' && char !== ' ' }, islower (char) { char = String.fromCharCode(char) return char.length === 1 && char === char.toLowerCase() }, isprint (char) { char = String.fromCharCode(char) return char.length === 1 && char >= ' ' && char <= '~' }, ispunct (char) { char = String.fromCharCode(char) return char.length === 1 && /[!@#$%^&*()_+{}[]:;<>,.?~\\\/-=]/.test(char) }, isspace (char) { char = String.fromCharCode(char) return char.length === 1 && /\s/.test(char) }, isupper (char) { char = String.fromCharCode(char) return char.length === 1 && char === char.toUpperCase() }, isxdigit (char) { char = String.fromCharCode(char) return char.length === 1 && /[0-9a-fA-F]/.test(char) }, toascii (char) { char = String.fromCharCode(char) return char.length === 1 ? char.charCodeAt(0) & 0x7F : -1 }, tolower (char) { char = String.fromCharCode(char) return char.length === 1 ? char.toLowerCase() : char }, toupper (char) { char = String.fromCharCode(char) return char.length === 1 ? char.toUpperCase() : char } }) // <errno.h> Object.assign(imports.env, { __errno_location () { if (!errnoPointer) { errnoPointer = env.adapter.heap.alloc(4) } return errnoPointer } }) // <libgen.h> Object.assign(imports.env, { basename (pathStringPointer) { if (!basenamePointer) { basenamePointer = env.adapter.heap.alloc(PATH_MAX + 1) } env.adapter.heap.set(basenamePointer, 0, PATH_MAX) if (!pathStringPointer) { env.adapter.setString(basenamePointer, '.') } else { const string = env.adapter.getString(pathStringPointer) if (string) { if (string === '/') { env.adapter.setString(basenamePointer, '/') } else if (string === '.') { env.adapter.setString(basenamePointer, '.') } else { const basename = path.basename(string) if (basename.length > PATH_MAX) { env.adapter.setUint8(imports.env.__errno_location(), ENAMETOOLONG) return NULL } env.adapter.setString(basenamePointer, basename) } } else { env.adapter.setString(basenamePointer, '.') } } return basenamePointer }, dirname (pathStringPointer) { if (!dirnamePointer) { dirnamePointer = env.adapter.heap.alloc(PATH_MAX + 1) } env.adapter.heap.set(dirnamePointer, 0, PATH_MAX) if (!pathStringPointer) { env.adapter.setString(dirnamePointer, '.') } else { const string = env.adapter.getString(pathStringPointer) if (string) { if (string === '/') { env.adapter.setString(dirnamePointer, '/') } else if (string === '.') { env.adapter.setString(dirnamePointer, '.') } else { const dirname = path.dirname(string) if (dirname.length > PATH_MAX) { env.adapter.setUint8(imports.env.__errno_location(), ENAMETOOLONG) return NULL } env.adapter.setString(dirnamePointer, dirname) } } else { env.adapter.setString(dirnamePointer, '.') } } return dirnamePointer } }) // <math.h> Object.assign(imports.env, { __eqtf2 (x, y) { imports.env.abort() }, __trunctfsf2 (low, high) { imports.env.abort() }, __trunctfdf2 (low, high) { imports.env.abort() }, acos (value) { return Math.acos(value) }, acosl (value) { return Math.acos(value) }, acosf (value) { return Math.acos(value) }, agcosl (value) { return Math.acos(value) }, acosh (value) { return Math.acosh(value) }, acoshf (value) { return Math.acosh(value) }, acoshl (value) { return Math.acosh(value) }, asin (value) { return Math.asin(value) }, asinf (value) { return Math.asin(value) }, asinl (value) { return Math.asin(value) }, asinh (value) { return Math.asinh(value) }, asinhf (value) { return Math.asinh(value) }, asinhl (value) { return Math.asinh(value) }, atan (value) { return Math.atan(value) }, atanf (value) { return Math.atan(value) }, atanl (value) { return Math.atan(value) }, atanh (value) { return Math.atanh(value) }, atanhf (value) { return Math.atanh(value) }, atanhl (value) { return Math.atanh(value) }, atan2 (x, y) { return Math.atan2(x, y) }, atan2f (x, y) { return Math.atan2(x, y) }, atan2l (x, y) { return Math.atan2(x, y) }, cbrt (value) { return Math.cbrt(value) }, cbrtf (value) { return Math.cbrt(value) }, cbrtl (value) { return Math.cbrt(value) }, ceil (value) { return Math.ceil(value) }, ceilf (value) { return Math.ceil(value) }, ceill (value) { return Math.ceil(value) }, copysign (x, y) { if (y === 0) { return Math.abs(x) } if (x === 0) { return 0 } return Math.abs(x) * Math.sign(y) }, copysignf (x, y) { return imports.env.copysign(x, y) }, copysignl (x, y) { return imports.env.copysign(x, y) }, cos (value) { return Math.cos(value) }, cosf (value) { return Math.cos(value) }, cosl (value) { return Math.cos(value) }, cosh (value) { return Math.cosh(value) }, coshf (value) { return Math.cosh(value) }, coshl (value) { return Math.cosh(value) }, erf (value) { // constants for the rational approximations const a1 = 0.254829592 const a2 = -0.284496736 const a3 = 1.421413741 const a4 = -1.453152027 const a5 = 1.061405429 // approximation formula const x = value const t = 1.0 / (1.0 + 0.3275911 * x) const result = (a1 * t + a2 * t * t + a3 * t * t * t + a4 * t * t * t * t + a5 * t * t * t * t * t) * Math.exp(-x * x) return x >= 0 ? 1.0 - result : result - 1.0 }, erff (value) { return imports.env.erf(value) }, erfl (value) { return imports.env.erf(value) }, erfcf (value) { return 1 - imports.env.erf(value) }, erfcl (value) { return 1 - imports.env.erf(value) }, exp (value) { return Math.exp(value) }, expf (value) { return Math.exp(value) }, expl (value) { return Math.exp(value) }, exp2 (value) { const x = value * 1.4426950408889634 // approximation of log2(2) const clamped = Math.max(-1022, Math.min(1023, x)) const bias = clamped + 1023 return new Float64Array(Float64Array.of(bias << 52).buffer)[0] }, exp2f (value) { return imports.env.exp2(value) }, exp2l (value) { return imports.env.exp2(value) }, expm1 (value) { return Math.expm1(value) }, expm1f (value) { return Math.expm1(value) }, expm1l (value) { return Math.expm1(value) }, fabs (value) { return Math.abs(value) }, fabsf (value) { return Math.abs(value) }, fabsl (value) { return Math.abs(value) }, fdim (x, y) { return x > y ? x - y : 0.0 }, fdimf (x, y) { return x > y ? x - y : 0.0 }, fdiml (x, y) { return x > y ? x - y : 0.0 }, floor (value) { return Math.floor(value) }, floorf (value) { return Math.floor(value) }, floorl (value) { return Math.floor(value) }, fma (x, y, z) { return x * y + z }, fmaf (x, y, z) { return x * y + z }, fmal (x, y, z) { return x * y + z }, fmax (x, y) { return Math.max(x, y) }, fmaxf (x, y) { return Math.max(x, y) }, fmaxl (x, y) { return Math.max(x, y) }, fmin (x, y) { return Math.min(x, y) }, fminf (x, y) { return Math.min(x, y) }, fminl (x, y) { return Math.min(x, y) },