/** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */ import koffi from 'koffi' import * as abi from './abi.ts' import { Win32Error } from './errors.ts' declare const nativePtr: unique symbol /** Koffi native pointer branded against accidental numeric use. */ export type NativePtr = bigint & { readonly [nativePtr]: true } type Ptr = ReturnType const PVOID: Ptr = koffi.pointer('void') const PPVOID: Ptr = koffi.pointer(PVOID) /** Loaded Win32 libraries and the shared stdcall binder used by process extensions. */ export interface Win32BindingContext { /** Kernel process, handle, pipe, and Job APIs. */ readonly kernel32: ReturnType /** Token and security APIs. */ readonly advapi32: ReturnType /** Bind one stdcall function from a loaded Win32 library. */ readonly bind: ( library: ReturnType, name: string, result: Ptr | string, args: Array, ) => unknown } /** * Return whether a Koffi pointer represents NULL. * @param value - pointer value returned by Koffi or a Win32 call. * @returns true for null, undefined, or address zero. */ export function isNullPtr(value: NativePtr | null | undefined): value is null | undefined { return value === null || value === undefined || (value as bigint) === 0n } /** STARTUPINFOW fields used by inherited or piped stdio launches. */ export interface StartupInfoInput { cb: number dwFlags: number hStdInput: NativePtr hStdOutput: NativePtr hStdError: NativePtr } /** Decoded PROCESS_INFORMATION result. */ export interface ProcessInfoOutput { hProcess: NativePtr | null hThread: NativePtr | null dwProcessId: number dwThreadId: number } /** Generic Win32 calls consumed by restricted-token sandbox process operations. */ export interface Win32ProcessBindings { closeHandle(handle: NativePtr): number getLastError(): number formatMessageW( flags: number, source: null, messageId: number, languageId: number, buffer: Buffer, size: number, args: null, ): number createPipe(readHandle: NativePtr, writeHandle: NativePtr, attributes: null, size: number): number setHandleInformation(handle: NativePtr, mask: number, flags: number): number createProcessAsUserW( token: NativePtr, applicationName: null, commandLine: string, processAttributes: null, threadAttributes: null, inheritHandles: number, creationFlags: number, environment: null, currentDirectory: string | null, startupInfo: NativePtr, processInfo: NativePtr, ): number readFile(file: NativePtr, buffer: Buffer, count: number, bytesRead: NativePtr, overlapped: null): number peekNamedPipe( pipe: NativePtr, buffer: null, size: number, bytesRead: NativePtr | null, totalAvail: NativePtr, leftThisMessage: NativePtr | null, ): number waitForSingleObject(handle: NativePtr, milliseconds: number): number getExitCodeProcess(process: NativePtr, exitCode: NativePtr): number createJobObjectW(attributes: null, name: null): NativePtr setInformationJobObject(job: NativePtr, cls: number, information: Buffer, length: number): number assignProcessToJobObject(job: NativePtr, process: NativePtr): number resumeThread(thread: NativePtr): number terminateProcess(process: NativePtr, exitCode: number): number getStdHandle(stdHandle: number): NativePtr } /** Koffi STARTUPINFOW layout. */ export const STARTUPINFOW = koffi.struct('DSH_STARTUPINFOW', { cb: 'uint32', lpReserved: 'str16', lpDesktop: 'str16', lpTitle: 'str16', dwX: 'uint32', dwY: 'uint32', dwXSize: 'uint32', dwYSize: 'uint32', dwXCountChars: 'uint32', dwYCountChars: 'uint32', dwFillAttribute: 'uint32', dwFlags: 'uint32', wShowWindow: 'uint16', cbReserved2: 'uint16', lpReserved2: koffi.pointer('uint8'), hStdInput: PVOID, hStdOutput: PVOID, hStdError: PVOID, }) /** Koffi PROCESS_INFORMATION layout. */ export const PROCESS_INFORMATION = koffi.struct('DSH_PROCESS_INFORMATION', { hProcess: PVOID, hThread: PVOID, dwProcessId: 'uint32', dwThreadId: 'uint32', }) /* v8 ignore start -- ABI guards are pinned by native header probes. */ if (STARTUPINFOW.size !== abi.STARTUPINFOW_SIZE) { throw new Error(`STARTUPINFOW layout mismatch: koffi computed ${STARTUPINFOW.size}, expected ${abi.STARTUPINFOW_SIZE}`) } if (PROCESS_INFORMATION.size !== abi.PROCESS_INFORMATION_SIZE) { throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, expected ${abi.PROCESS_INFORMATION_SIZE}`) } /* v8 ignore stop */ /** * Allocate a pointer-sized out-parameter slot. * @returns allocated native slot. */ export function allocPtrSlot(): NativePtr { return koffi.alloc(PVOID, 1) as NativePtr } /** * Allocate a uint32 out-parameter slot. * @returns allocated native slot. */ export function allocUint32(): NativePtr { return koffi.alloc('uint32', 1) as NativePtr } /** * Decode a pointer out-parameter. * @param slot - pointer-sized slot filled by Win32. * @returns decoded pointer, or null for address zero. */ export function decodePtr(slot: NativePtr): NativePtr | null { const value = koffi.decode(slot, PVOID) as NativePtr | null return isNullPtr(value) ? null : value } /** * Decode a uint32 out-parameter. * @param slot - uint32 slot filled by Win32. * @returns decoded unsigned value. */ export function decodeUint32(slot: NativePtr): number { return koffi.decode(slot, 'uint32') as number } /** * Allocate a zeroed STARTUPINFOW. * @returns allocated struct pointer. */ export function allocStartupInfo(): NativePtr { return koffi.alloc(STARTUPINFOW, 1) as NativePtr } /** * Encode the stdio-bearing STARTUPINFOW fields. * @param startupInfo - allocated STARTUPINFOW pointer. * @param fields - fields required for inherited stdio. */ export function encodeStartupInfo(startupInfo: NativePtr, fields: StartupInfoInput): void { koffi.encode(startupInfo, STARTUPINFOW, fields) } /** * Allocate a zeroed PROCESS_INFORMATION. * @returns allocated struct pointer. */ export function allocProcessInfo(): NativePtr { return koffi.alloc(PROCESS_INFORMATION, 1) as NativePtr } /** * Decode PROCESS_INFORMATION. * @param processInfo - struct pointer filled by CreateProcess. * @returns process/thread handles and ids. */ export function decodeProcessInfo(processInfo: NativePtr): ProcessInfoOutput { return koffi.decode(processInfo, PROCESS_INFORMATION) as ProcessInfoOutput } let cachedContext: Win32BindingContext | undefined let cached: Win32ProcessBindings | undefined /* v8 ignore start -- exercised by native Windows ABI and sandbox jobs. */ function bindingContext(): Win32BindingContext { if (cachedContext !== undefined) return cachedContext const kernel32 = koffi.load('kernel32.dll') const advapi32 = koffi.load('advapi32.dll') const bind = ( lib: ReturnType, name: string, result: Ptr | string, args: Array, ): unknown => lib.func('__stdcall', name, result, args) cachedContext = { kernel32, advapi32, bind } return cachedContext } function bindings(): Win32ProcessBindings { if (cached !== undefined) return cached const { kernel32, advapi32, bind } = bindingContext() cached = { closeHandle: bind(kernel32, 'CloseHandle', 'int', [PVOID]), getLastError: bind(kernel32, 'GetLastError', 'uint32', []), formatMessageW: bind(kernel32, 'FormatMessageW', 'uint32', [ 'uint32', PVOID, 'uint32', 'uint32', PVOID, 'uint32', PVOID, ]), createPipe: bind(kernel32, 'CreatePipe', 'int', [PPVOID, PPVOID, PVOID, 'uint32']), setHandleInformation: bind(kernel32, 'SetHandleInformation', 'int', [PVOID, 'uint32', 'uint32']), createProcessAsUserW: bind(advapi32, 'CreateProcessAsUserW', 'int', [ PVOID, 'str16', 'str16', PVOID, PVOID, 'int', 'uint32', PVOID, 'str16', koffi.pointer(STARTUPINFOW), koffi.pointer(PROCESS_INFORMATION), ]), readFile: bind(kernel32, 'ReadFile', 'int', [PVOID, PVOID, 'uint32', koffi.pointer('uint32'), PVOID]), peekNamedPipe: bind(kernel32, 'PeekNamedPipe', 'int', [ PVOID, PVOID, 'uint32', koffi.pointer('uint32'), koffi.pointer('uint32'), koffi.pointer('uint32'), ]), waitForSingleObject: bind(kernel32, 'WaitForSingleObject', 'uint32', [PVOID, 'uint32']), getExitCodeProcess: bind(kernel32, 'GetExitCodeProcess', 'int', [PVOID, koffi.pointer('uint32')]), createJobObjectW: bind(kernel32, 'CreateJobObjectW', PVOID, [PVOID, 'str16']), setInformationJobObject: bind(kernel32, 'SetInformationJobObject', 'int', [PVOID, 'int', PVOID, 'uint32']), assignProcessToJobObject: bind(kernel32, 'AssignProcessToJobObject', 'int', [PVOID, PVOID]), resumeThread: bind(kernel32, 'ResumeThread', 'uint32', [PVOID]), terminateProcess: bind(kernel32, 'TerminateProcess', 'int', [PVOID, 'uint32']), getStdHandle: bind(kernel32, 'GetStdHandle', PVOID, ['int']), } as unknown as Win32ProcessBindings return cached } /** * Extend the shared process table with caller-owned Win32 API families. * @param create - binds only the caller-specific operations from the shared libraries. * @returns generic process bindings combined with the caller-specific operations. */ export function extendWin32ProcessBindings( create: (context: Win32BindingContext) => Extension, ): Win32ProcessBindings & Extension { return { ...bindings(), ...create(bindingContext()) } } /* v8 ignore stop */ /** * Format a Win32 error code through FormatMessageW. * @param api - active binding table. * @param win32Code - captured GetLastError value. * @returns trimmed system message, or an empty string when unavailable. */ export function errorText(api: Win32ProcessBindings, win32Code: number): string { const buffer = Buffer.alloc(1024) const length = api.formatMessageW( abi.FORMAT_MESSAGE_FROM_SYSTEM | abi.FORMAT_MESSAGE_IGNORE_INSERTS, null, win32Code, 0, buffer, buffer.length / 2, null, ) return length === 0 ? '' : buffer.subarray(0, length * 2).toString('utf16le').trim() } /** * Throw the current GetLastError value. * @param api - active binding table. * @param name - failing Win32 operation. * @param detail - optional operation context. * @returns never; always throws Win32Error. */ export function throwLastError(api: Win32ProcessBindings, name: string, detail?: string): never { const win32Code = api.getLastError() throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code)) } /** * Throw an explicitly captured Win32 error code. * @param api - active binding table. * @param name - failing Win32 operation. * @param win32Code - error captured before cleanup. * @param detail - optional operation context. * @returns never; always throws Win32Error. */ export function throwWin32( api: Win32ProcessBindings, name: string, win32Code: number, detail?: string, ): never { throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code)) }