From bc76e9c79ad7e0422953929cc2d39d0d6db62301 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Sat, 11 Apr 2026 20:17:06 -0700 Subject: [PATCH] Shared text clipboard via VMware backdoor + W95TOOLS.EXE guest agent (#361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three layers: v86 — src/vmware.js gains the legacy text-clipboard backdoor commands (GETSELLENGTH/GETNEXTPIECE/SETSELLENGTH/SETNEXTPIECE, 6–9). The host stages bytes via the vmware-clipboard-host bus event; the guest pushes via 8/9 and the device emits vmware-clipboard-guest when the buffer fills. Same wire protocol as open-vm-tools' pre-RPC copy/paste. Committed on the windows95-base fork branch; libv86.js rebuilt here. renderer — src/renderer/clipboard.ts polls Electron's clipboard (no change event exists), translates host UTF-8/LF ↔ guest CP-1252/CRLF, and bounces bytes through the two bus events. Echo-suppressed so a value we just wrote does not come back as a change. guest — guest-tools/agent/W95TOOLS.EXE is a 22 KB hidden-window agent that joins the Win32 clipboard-viewer chain (push-on-copy) and polls the backdoor on a 250 ms timer (pull-from-host). Win9x runs ring-3 with the I/O bitmap wide open, so a plain IN EAX,DX from a user process reaches the port — no driver needed. Named for growth: time sync and host-initiated shutdown will live here too. Built with Open Watcom v2 inside Docker (Makefile + Dockerfile alongside the source); subsystem 4.0, no msvcrt, runs on Win95 RTM. Install: copy \\HOST\TOOLS\agent\W95TOOLS.EXE into the guest and drop a shortcut in StartUp. Text only, 64 KB cap. --- .claude/skills/update-v86/SKILL.md | 10 +- guest-tools/README.md | 20 ++++ guest-tools/agent/Dockerfile | 12 ++ guest-tools/agent/Makefile | 30 +++++ guest-tools/agent/W95TOOLS.EXE | Bin 0 -> 22528 bytes guest-tools/agent/w95tools.c | 181 +++++++++++++++++++++++++++++ src/renderer/clipboard.ts | 62 ++++++++++ src/renderer/emulator.tsx | 5 + src/renderer/lib/libv86.js | 12 +- 9 files changed, 322 insertions(+), 10 deletions(-) create mode 100644 guest-tools/agent/Dockerfile create mode 100644 guest-tools/agent/Makefile create mode 100644 guest-tools/agent/W95TOOLS.EXE create mode 100644 guest-tools/agent/w95tools.c create mode 100644 src/renderer/clipboard.ts diff --git a/.claude/skills/update-v86/SKILL.md b/.claude/skills/update-v86/SKILL.md index 32ff1eb..6dfbc29 100644 --- a/.claude/skills/update-v86/SKILL.md +++ b/.claude/skills/update-v86/SKILL.md @@ -35,11 +35,13 @@ That branch merges four feature branches, each upstreamable on its own: writes to target only `current_interface`, but per ATA spec they're channel-shared (one register file on the IDE cable; both drives latch the same value). -- **`vmware-abspointer`** — `src/vmware.js` implements the VMware mouse - backdoor (port `0x5658`, GETVERSION + ABSPOINTER_*) so a guest driver +- **`vmware-abspointer`** — `src/vmware.js` implements the VMware + backdoor (port `0x5658`): GETVERSION + ABSPOINTER_* so a guest driver (VBADOS VBMOUSE) can read absolute cursor position and track the host - cursor 1:1 without pointer lock. Consumes the `mouse-absolute` bus - event that `MouseAdapter` already emits. + cursor 1:1 without pointer lock, and the legacy text-clipboard commands + 6–9 so `W95TOOLS.EXE` (guest-tools/agent) can sync `CF_TEXT` with + the host. Consumes `mouse-absolute` and `vmware-clipboard-host` bus + events; emits `vmware-absolute-mouse` and `vmware-clipboard-guest`. - **`vga-defer-vbe-disable-v86`** — `src/vga.js` defers `dispi[4]=0` written from V86 mode until a legacy attribute-mode write reaches the hardware. Win9x's VDD virtualises ports 3B0–3DF for a windowed DOS VM diff --git a/guest-tools/README.md b/guest-tools/README.md index cb58cf6..ed69af1 100644 --- a/guest-tools/README.md +++ b/guest-tools/README.md @@ -21,3 +21,23 @@ Install inside the guest: absolute mouse driver**. 3. Reboot. The app detects the driver and stops grabbing pointer lock; ESC still toggles lock for games that want raw relative input. + +## agent/ — W95TOOLS guest agent + +`W95TOOLS.EXE` is a hidden-window agent that talks to the emulator over +the VMware backdoor (port 0x5658). Currently it does one thing: bridges +Windows 95's `CF_TEXT` clipboard to the host (legacy backdoor commands +6–9; host side is `src/renderer/clipboard.ts`, which polls Electron's +clipboard). It's also where time sync, host-initiated shutdown, and a +tray icon will live when those land. + +Install inside the guest: + +1. Copy `\\HOST\TOOLS\agent\W95TOOLS.EXE` to `C:\WINDOWS\`. +2. Drop a shortcut to it in + `C:\WINDOWS\Start Menu\Programs\StartUp` so it runs on login. + +Copy text on either side and it appears on the other within ~250 ms. +Text only; conversion is Windows-1252 ↔ UTF-8 with CRLF ↔ LF, capped at +64 KB. Built from `w95tools.c` with Open Watcom v2 — `make -C +guest-tools/agent` (needs Docker). diff --git a/guest-tools/agent/Dockerfile b/guest-tools/agent/Dockerfile new file mode 100644 index 0000000..8337c94 --- /dev/null +++ b/guest-tools/agent/Dockerfile @@ -0,0 +1,12 @@ +FROM --platform=linux/amd64 debian:bookworm-slim +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl xz-utils ca-certificates make && \ + rm -rf /var/lib/apt/lists/* +RUN mkdir -p /opt/watcom && \ + curl -fsSL https://github.com/open-watcom/open-watcom-v2/releases/download/Current-build/ow-snapshot.tar.xz \ + | tar -xJ -C /opt/watcom +ENV WATCOM=/opt/watcom +ENV PATH=$WATCOM/binl64:$PATH +ENV INCLUDE=$WATCOM/h:$WATCOM/h/nt +ENV LIB=$WATCOM/lib386:$WATCOM/lib386/nt +WORKDIR /work diff --git a/guest-tools/agent/Makefile b/guest-tools/agent/Makefile new file mode 100644 index 0000000..6435c04 --- /dev/null +++ b/guest-tools/agent/Makefile @@ -0,0 +1,30 @@ +# Build W95TOOLS.EXE for Windows 95 with Open Watcom v2, inside Docker. +# +# Watcom is the only readily-available cross-compiler that emits a PE binary +# Win95 RTM will load (subsystem 4.0, no msvcrt). mingw-w64 targets NT. The +# macOS-native Watcom binaries are unsigned and Gatekeeper kills them, so we +# run the linux/amd64 build under Docker instead. + +IMAGE := windows95-ow2 +# Docker Desktop's bind-mount file sync races with recently-edited files; work +# around it by piping the source on stdin and building in /tmp inside the +# container, then dumping the EXE bytes back over stdout. +DOCKER := docker run --rm -i --platform linux/amd64 $(IMAGE) +CFLAGS := -bt=nt -3r -zq -wx -we -os -s + +.PHONY: all image clean + +all: W95TOOLS.EXE + +image: + docker build --platform linux/amd64 -t $(IMAGE) . + +W95TOOLS.EXE: w95tools.c image + $(DOCKER) sh -c 'cd /tmp && cat >w95tools.c && \ + wcc386 $(CFLAGS) w95tools.c && \ + wlink system nt_win option quiet name W95TOOLS.EXE \ + file w95tools.o library kernel32,user32 && \ + cat W95TOOLS.EXE' $@ + +clean: + rm -f W95TOOLS.EXE diff --git a/guest-tools/agent/W95TOOLS.EXE b/guest-tools/agent/W95TOOLS.EXE new file mode 100644 index 0000000000000000000000000000000000000000..d8903de0c1c6ea78b30674c563466d4b714f2926 GIT binary patch literal 22528 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�YmrQI!jTJyvSlGa=h9&^XgYmA6`j@0Z_@qs~yp6 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 L^8fz+DF^-^X{mz= literal 0 HcmV?d00001 diff --git a/guest-tools/agent/w95tools.c b/guest-tools/agent/w95tools.c new file mode 100644 index 0000000..29edaad --- /dev/null +++ b/guest-tools/agent/w95tools.c @@ -0,0 +1,181 @@ +/* + * W95TOOLS — guest-side integration agent for the windows95 emulator. + * + * Currently: bidirectional text clipboard. Talks to the emulator over the + * legacy VMware backdoor (port 0x5658; implemented in v86's vmware.js). + * Joins the Win32 clipboard-viewer chain so guest copies are pushed + * immediately, and polls the backdoor on a timer so host copies show up + * within ~250 ms. + * + * Win9x runs ring-3 code with the I/O bitmap wide open, so a plain IN works + * from a user process — no driver needed. On NT this would #GP; we don't run + * there. + * + * Build with Open Watcom v2 (see Makefile). Links USER32/KERNEL32 only, + * runs on Win95 RTM. + */ + +#define WIN32_LEAN_AND_MEAN +#include + +#define VMW_MAGIC 0x564D5868UL +#define VMW_PORT 0x5658 +#define CMD_GETLEN 6 +#define CMD_GETDATA 7 +#define CMD_SETLEN 8 +#define CMD_SETDATA 9 +#define CMD_VERSION 10 + +#define POLL_MS 250 +#define MAX_CLIP 0xFFFF + +extern unsigned long bd(unsigned long cmd, unsigned long arg); +#pragma aux bd = \ + "mov eax, 564D5868h" \ + "mov edx, 5658h" \ + "in eax, dx" \ + parm [ecx] [ebx] \ + value [eax] \ + modify [edx]; + +extern unsigned long bd_ebx(unsigned long cmd, unsigned long arg); +#pragma aux bd_ebx = \ + "mov eax, 564D5868h" \ + "mov edx, 5658h" \ + "in eax, dx" \ + parm [ecx] [ebx] \ + value [ebx] \ + modify [eax edx]; + +static HWND g_next; +static int g_ignore; + +static void push_to_host(HWND hwnd) +{ + HANDLE h; + char *p; + unsigned long len, i, w; + + if (!IsClipboardFormatAvailable(CF_TEXT)) { + bd(CMD_SETLEN, 0); + return; + } + if (!OpenClipboard(hwnd)) return; + h = GetClipboardData(CF_TEXT); + if (h && (p = (char *)GlobalLock(h)) != 0) { + len = lstrlen(p); + if (len > MAX_CLIP) len = MAX_CLIP; + bd(CMD_SETLEN, len); + for (i = 0; i < len; i += 4) { + w = (unsigned char)p[i]; + if (i + 1 < len) w |= (unsigned long)(unsigned char)p[i+1] << 8; + if (i + 2 < len) w |= (unsigned long)(unsigned char)p[i+2] << 16; + if (i + 3 < len) w |= (unsigned long)(unsigned char)p[i+3] << 24; + bd(CMD_SETDATA, w); + } + GlobalUnlock(h); + } + CloseClipboard(); +} + +static void pull_from_host(HWND hwnd) +{ + long len; + unsigned long i, w; + HGLOBAL h; + char *p; + + len = (long)bd(CMD_GETLEN, 0); + if (len < 0) return; + if (len > MAX_CLIP) len = MAX_CLIP; + + h = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, (DWORD)len + 1); + if (!h) return; + p = (char *)GlobalLock(h); + for (i = 0; i < (unsigned long)len; i += 4) { + w = bd(CMD_GETDATA, 0); + p[i] = (char)w; + if (i + 1 < (unsigned long)len) p[i+1] = (char)(w >> 8); + if (i + 2 < (unsigned long)len) p[i+2] = (char)(w >> 16); + if (i + 3 < (unsigned long)len) p[i+3] = (char)(w >> 24); + } + p[len] = 0; + GlobalUnlock(h); + + if (!OpenClipboard(hwnd)) { GlobalFree(h); return; } + g_ignore++; + EmptyClipboard(); + SetClipboardData(CF_TEXT, h); + CloseClipboard(); +} + +static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +{ + switch (msg) { + case WM_CREATE: + g_next = SetClipboardViewer(hwnd); + SetTimer(hwnd, 1, POLL_MS, 0); + return 0; + + case WM_DRAWCLIPBOARD: + if (g_ignore > 0) g_ignore--; + else push_to_host(hwnd); + if (g_next) SendMessage(g_next, msg, wp, lp); + return 0; + + case WM_CHANGECBCHAIN: + if ((HWND)wp == g_next) g_next = (HWND)lp; + else if (g_next) SendMessage(g_next, msg, wp, lp); + return 0; + + case WM_TIMER: + pull_from_host(hwnd); + return 0; + + case WM_DESTROY: + ChangeClipboardChain(hwnd, g_next); + KillTimer(hwnd, 1); + PostQuitMessage(0); + return 0; + } + return DefWindowProc(hwnd, msg, wp, lp); +} + +int PASCAL WinMain(HINSTANCE hi, HINSTANCE hp, LPSTR cmd, int show) +{ + WNDCLASS wc; + HWND hwnd; + MSG msg; + + (void)hp; (void)cmd; (void)show; + + if (CreateMutex(0, FALSE, "W95Tools") && GetLastError() == ERROR_ALREADY_EXISTS) + return 0; + + if (bd_ebx(CMD_VERSION, 0) != VMW_MAGIC) { + MessageBox(0, "VMware backdoor not present.", "W95Tools", MB_OK | MB_ICONSTOP); + return 1; + } + + wc.style = 0; + wc.lpfnWndProc = WndProc; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = hi; + wc.hIcon = 0; + wc.hCursor = 0; + wc.hbrBackground = 0; + wc.lpszMenuName = 0; + wc.lpszClassName = "W95Tools"; + RegisterClass(&wc); + + hwnd = CreateWindow("W95Tools", "W95Tools", WS_OVERLAPPED, + 0, 0, 0, 0, 0, 0, hi, 0); + if (!hwnd) return 1; + + while (GetMessage(&msg, 0, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return 0; +} diff --git a/src/renderer/clipboard.ts b/src/renderer/clipboard.ts new file mode 100644 index 0000000..33ea9a5 --- /dev/null +++ b/src/renderer/clipboard.ts @@ -0,0 +1,62 @@ +// Bidirectional text clipboard between the host and the Win95 guest. +// +// Transport is the legacy VMware backdoor clipboard protocol (port 0x5658, +// commands 6–9) implemented in v86's vmware.js. Inside the guest, W95TOOLS.EXE +// (guest-tools/agent) polls the backdoor and bridges it to CF_TEXT via the +// Win32 clipboard-viewer chain. Out here we poll Electron's clipboard — there +// is no change event — and translate between host UTF-8/LF and guest +// Windows-1252/CRLF. + +import { clipboard } from "electron"; + +const CP1252 = new TextDecoder("windows-1252"); +// v86's vmware.js and the guest agent both clamp to 64 KB; clamp here too so +// a huge host clipboard never even gets allocated/encoded. +const CLIP_MAX = 0x10000; + +function fromGuest(bytes: Uint8Array): string { + return CP1252.decode(bytes).replace(/\r\n/g, "\n"); +} + +function toGuest(text: string): Uint8Array { + const s = text.slice(0, CLIP_MAX).replace(/\r\n|\n/g, "\r\n"); + const n = Math.min(s.length, CLIP_MAX); + const out = new Uint8Array(n); + for (let i = 0; i < n; i++) { + const c = s.charCodeAt(i); + out[i] = c < 256 ? c : 0x3f; + } + return out; +} + +export function setupClipboardSync(emulator: any): () => void { + // Track the last value seen on each side so a value we just wrote doesn't + // bounce back as a "change" from the other side. + let lastHost = clipboard.readText(); + let lastGuest = ""; + + emulator.add_listener("vmware-clipboard-guest", (bytes: Uint8Array) => { + const text = fromGuest(bytes); + if (text === lastHost || text === lastGuest) return; + lastGuest = text; + lastHost = text; + clipboard.writeText(text); + console.log("[clip] guest → host", text.length, "chars"); + }); + + const poll = () => { + const text = clipboard.readText(); + if (text === lastHost) return; + lastHost = text; + if (text === lastGuest) return; + emulator.bus.send("vmware-clipboard-host", toGuest(text)); + console.log("[clip] host → guest", text.length, "chars"); + }; + + const id = window.setInterval(poll, 500); + window.addEventListener("focus", poll); + return () => { + window.clearInterval(id); + window.removeEventListener("focus", poll); + }; +} diff --git a/src/renderer/emulator.tsx b/src/renderer/emulator.tsx index 9d3fdb1..814db53 100644 --- a/src/renderer/emulator.tsx +++ b/src/renderer/emulator.tsx @@ -19,6 +19,7 @@ import { resetState } from "./utils/reset-state"; import { setupSmbShare } from "./smb"; import { setupTcpRelay } from "./net/tcp-relay"; import { setupDnsShim } from "./net/dns-shim"; +import { setupClipboardSync } from "./clipboard"; import { startProbe } from "./debug-harness"; import { SyncFileBuffer } from "./sync-file-buffer"; @@ -406,6 +407,10 @@ export class Emulator extends React.Component<{}, EmulatorState> { startProbe(window["emulator"]); } + // Host ↔ guest text clipboard. The guest side is W95TOOLS.EXE on the + // TOOLS share; until that's running this is a no-op poller. + setupClipboardSync(window["emulator"]); + // New v86 instance // Mouse stays disabled until either the pointer is captured or the guest's // VMware-backdoor mouse driver requests absolute mode. diff --git a/src/renderer/lib/libv86.js b/src/renderer/lib/libv86.js index c76c6f6..d4a4684 100644 --- a/src/renderer/lib/libv86.js +++ b/src/renderer/lib/libv86.js @@ -426,13 +426,13 @@ this.mouse_clicks=this.mouse_delta_x=this.mouse_delta_y=0;break;case 245:this.en 0;break;default:y(a)}this.mouse_irq()}}else if(this.read_controller_output_port)this.read_controller_output_port=!1,this.controller_output_port=a;else{y(a);this.mouse_buffer.clear();this.kbd_buffer.clear();this.kbd_buffer.push(250);switch(a){case 237:this.next_read_led=!0;break;case 240:this.next_handle_scan_code_set=!0;break;case 242:this.kbd_buffer.push(171);this.kbd_buffer.push(131);break;case 243:this.next_read_rate=!0;break;case 244:this.enable_keyboard_stream=!0;break;case 245:this.enable_keyboard_stream= !1;break;case 246:break;case 255:this.kbd_buffer.clear();this.kbd_buffer.push(250);this.kbd_buffer.push(170);this.kbd_buffer.push(0);break;default:y(a)}this.kbd_irq()}}; Gc.prototype.port64_write=function(a){y(a);switch(a){case 32:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(this.command_register);this.kbd_irq();break;case 96:this.read_command_register=!0;break;case 209:this.read_controller_output_port=!0;break;case 211:this.read_output_register=!0;break;case 212:this.next_is_mouse_command=!0;break;case 167:this.command_register|=32;break;case 168:this.command_register&=-33;break;case 169:this.kbd_buffer.clear();this.mouse_buffer.clear(); -this.kbd_buffer.push(0);this.kbd_irq();break;case 170:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(85);this.kbd_irq();break;case 171:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(0);this.kbd_irq();break;case 173:this.command_register|=16;break;case 174:this.command_register&=-17;break;case 254:this.cpu.reboot_internal();break;default:y(a)}};function Hc(a,b){this.cpu=a;this.bus=b;this.absolute=this.enabled=!1;this.queue=[];this.buttons=0;this.last_y=this.last_x=-1;this.tail_is_move=!1;this.clip_out=new Uint8Array(0);this.clip_out_cursor=0;this.clip_out_fresh=!1;this.clip_in=new Uint8Array(0);this.clip_in_cursor=0;this.bus.register("vmware-clipboard-host",function(c){this.clip_out=65536this.last_x||(b&&this.tail_is_move&&4<=this.queue.length?(this.queue[this.queue.length-3]=this.last_x,this.queue[this.queue.length-2]=this.last_y):1024>>0,65536),this.clip_in=new Uint8Array(a),this.clip_in_cursor=0,0===a&&this.bus.send("vmware-clipboard-guest", +this.kbd_buffer.push(0);this.kbd_irq();break;case 170:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(85);this.kbd_irq();break;case 171:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(0);this.kbd_irq();break;case 173:this.command_register|=16;break;case 174:this.command_register&=-17;break;case 254:this.cpu.reboot_internal();break;default:y(a)}};function Hc(a,b){function c(){}this.cpu=a;this.bus=b;this.absolute=this.enabled=!1;this.queue=[];this.buttons=0;this.last_y=this.last_x=-1;this.tail_is_move=!1;this.clip_out=new Uint8Array(0);this.clip_out_cursor=0;this.clip_out_fresh=!1;this.clip_in=new Uint8Array(0);this.clip_in_cursor=0;this.bus.register("vmware-clipboard-host",function(d){this.clip_out=65536this.last_x||(b&&this.tail_is_move&&4<=this.queue.length?(this.queue[this.queue.length-3]=this.last_x,this.queue[this.queue.length-2]=this.last_y):1024>>0,65536),this.clip_in=new Uint8Array(a),this.clip_in_cursor=0,0===a&&this.bus.send("vmware-clipboard-guest", this.clip_in),0;case 9:b=this.clip_in;a=a[3]>>>0;var c=this.clip_in_cursor;c>>8);c>>16);c>>24);this.clip_in_cursor=c;c>=b.length&&(this.bus.send("vmware-clipboard-guest",b),this.clip_in=new Uint8Array(0),this.clip_in_cursor=0);return 0;case 40:return this.enabled?this.queue.length:-65536;case 39:b=Math.min(a[3]>>>0,4,this.queue.length);c=[0,0,0,0];for(let d=0;d-1}}(7)},{type:Kc},{machine:Kc},{version1:U},{entry:U},{phoff:U},{shoff:U},{flags:U},{ehsize:Kc},{phentsize:Kc},{phnum:Kc},{shentsize:Kc},{shnum:Kc},{shstrndx:Kc}]);console.assert(52===Mc.reduce((a,b)=>a+b.size,0)); +c[3];return c[0];case 41:switch(a[3]){case 1161905490:this.enabled=!0;this.queue.length=0;this.tail_is_move=!1;this.queue.push(876762442);break;case 245:this.absolute=this.enabled=!1;this.queue.length=0;this.bus.send("vmware-absolute-mouse",!1);break;case 1396851026:this.absolute=!0;this.bus.send("vmware-absolute-mouse",!0);break;case 1279611474:this.absolute=!1,this.bus.send("vmware-absolute-mouse",!1)}return 0}return-1};Hc.prototype.get_state=function(){return[this.enabled,this.absolute]}; +Hc.prototype.set_state=function(a){this.enabled=a[0];this.absolute=a[1];this.bus.send("vmware-absolute-mouse",this.absolute)};const Ic=DataView.prototype,Jc={size:1,get:Ic.getUint8,set:Ic.setUint8},Kc={size:2,get:Ic.getUint16,set:Ic.setUint16},U={size:4,get:Ic.getUint32,set:Ic.setUint32},Mc=Lc([{magic:U},{class:Jc},{data:Jc},{version0:Jc},{osabi:Jc},{abiversion:Jc},{pad0:function(a){return{size:a,get:()=>-1}}(7)},{type:Kc},{machine:Kc},{version1:U},{entry:U},{phoff:U},{shoff:U},{flags:U},{ehsize:Kc},{phentsize:Kc},{phnum:Kc},{shentsize:Kc},{shnum:Kc},{shstrndx:Kc}]);console.assert(52===Mc.reduce((a,b)=>a+b.size,0)); const Nc=Lc([{type:U},{offset:U},{vaddr:U},{paddr:U},{filesz:U},{memsz:U},{flags:U},{align:U}]);console.assert(32===Nc.reduce((a,b)=>a+b.size,0));const Oc=Lc([{name:U},{type:U},{flags:U},{addr:U},{offset:U},{size:U},{link:U},{info:U},{addralign:U},{entsize:U}]);console.assert(40===Oc.reduce((a,b)=>a+b.size,0));function Lc(a){return a.map(function(b){var c=Object.keys(b);console.assert(1===c.length);c=c[0];b=b[c];console.assert(0