39 Commits

Author SHA1 Message Date
Felix Rieseberg
35c82c5d09 check-links: skip /releases/download/ URLs (chicken-and-egg with release builds) 2026-04-12 21:25:17 -07:00
Felix Rieseberg
60ee631575 Fetch disk image from private GitHub release; harden download scripts
OneDrive 1drv.ms links no longer serve raw bytes to headless clients after the SPO migration. The disk image now lives as a release asset on a private repo and is fetched via 'gh release download' using DISK_REPO + DISK_TAG vars and an IMAGES_REPO_TOKEN secret. Scripts fail fast on missing env, bad zip, or missing windows95.img.
2026-04-12 21:21:58 -07:00
Felix Rieseberg
dddaca9120 README: update download links for v5.0.0
Drops the Windows ARM64 row (not built in v5) and adds Linux ARM64/ARM rpm+deb links.
2026-04-12 18:38:28 -07:00
Felix Rieseberg
d383958f2b Remove committed guest-tools .EXE; drop now-unneeded windows-sign patch
VBMOUSE.EXE is the DOS TSR build artifact (only vbmouse.drv is referenced by OEMSETUP.INF). With no .exe under guest-tools/ the @electron/windows-sign exclude patch is no longer needed. .gitignore now covers all guest-tools/**/*.exe.
2026-04-12 17:53:18 -07:00
Felix Rieseberg
a1637b1de1 Add SMB READ_RAW support for faster bulk transfers (#366)
Win95's redirector was falling back to ~2.8KB core READs (no raw mode
advertised), so a 200KB copy took ~360 round-trips through the emulated
NIC/TCP stack (~10s). With READ_RAW it pulls up to 64KB per round-trip.

- server.ts: readRaw() handler (raw bytes only, 0-byte frame on error);
  negotiate now sets MaxRawSize=65535 + CAP_RAW_MODE (NT) and RawMode
  bit 0 (LM); per-packet hex capture gated on WIN95_SMB_CAPTURE
- smb.ts: CMD_READ_RAW constant
- test-standalone.ts: READ_RAW happy-path + bad-fid tests (57 pass)
2026-04-12 17:52:17 -07:00
Felix Rieseberg
fff371073d Patch @electron/windows-sign to skip guest-tools when signing
VBMOUSE.EXE and friends are 16-bit DOS/Win9x binaries served to the guest over SMB; signtool refuses them with 'file format cannot be signed'. The patch skips any path under guest-tools/ during file collection.
2026-04-12 17:49:01 -07:00
Felix Rieseberg
8153e91706 CI: wire up Azure Trusted Signing for Windows builds
Installs the Microsoft.Trusted.Signing.Client dlib via NuGet, locates signtool.exe from the Windows SDK, and passes AZURE_* secrets to the Make step. Removes the unused .pfx-based signing step.
2026-04-12 17:38:12 -07:00
Felix Rieseberg
fb701041c2 Use cwd-relative path in @electron/packager resedit patch
The patch hardcoded a local Windows path, breaking Windows CI builds. Now resolves tools/resedit.js relative to process.cwd() and uses process.execPath instead of 'node'. Also drops the deb maker diagnostic step now that Linux builds pass.
2026-04-12 16:56:20 -07:00
Felix Rieseberg
e1c30f701c Remove hardcoded TEMP path; expand deb maker diagnostic
The TEMP override in forge.config.js broke Windows builds on CI runners (mkdtemp ENOENT). The expanded diagnostic exercises require() from maker-base's own resolution context to surface the real Linux failure.
2026-04-12 14:23:55 -07:00
Felix Rieseberg
bbcc4f32cd Align all @electron-forge/* packages to 7.11.1
cli and maker-deb were pinned at 7.8.3 while other makers resolved to 7.11.1, leaving two copies of maker-base in the tree. The mismatch breaks maker-deb's isSupportedOnCurrentPlatform() check on Linux runners.
2026-04-12 14:16:31 -07:00
Felix Rieseberg
9c99b6cb65 CI: disable fail-fast and add deb maker diagnostic 2026-04-12 14:12:48 -07:00
Felix Rieseberg
2e01152d82 Regenerate package-lock.json to include all @rollup platform binaries
The lockfile only contained @rollup/rollup-darwin-arm64 because it was last regenerated on macOS, which trips npm's optional-dependency bug (npm/cli#4828) and breaks 'npm ci' on Linux/Windows runners.
2026-04-12 13:26:52 -07:00
Felix Rieseberg
63f02842bc Bump to v5.0.0 (STATE_VERSION 5) for new disk image 2026-04-12 13:13:23 -07:00
Felix Rieseberg
c847467de6 Recover user files from orphaned state-vN.bin after a version bump (#365)
* Detect orphaned state-vN.bin and offer file recovery to a host folder

When STATE_VERSION is bumped, users previously lost their C:\ silently.
The Welcome card now detects an older state file (v4+), explains what
happened, and offers a one-click recovery: spin up a throwaway v86
(no boot), restore the legacy state to populate the hda dirty-block
overlay, walk the FAT32 tree reading overlay-if-dirty-else-base, and
copy any file the guest ever wrote out to ~/Downloads/Recovered C Drive.

Directories are created lazily so empty branches never appear; success
and failure render in the panel (no native dialogs). The geometry
constraint that keeps overlay+new-base valid is documented next to
STATE_VERSION. Also makes the dev-mode CDP port overridable via
WIN95_DEBUG_PORT so worktree instances don't fight over 9222.

* prettier
2026-04-12 12:38:28 -07:00
Felix Rieseberg
fa0e4c691e Add tools/pack-disk.sh to build the images zip CI downloads via DISK_URL 2026-04-12 09:54:21 -07:00
Felix Rieseberg
6c964f99e9 Refresh home.htm: drop 'since the last version', bump Last Updated to 2026 2026-04-12 09:25:58 -07:00
Felix Rieseberg
1f116d607b Update shared-folder docs/UI: Z: is auto-mounted, drop \\HOST instructions 2026-04-12 09:24:32 -07:00
Felix Rieseberg
d38355ff16 Auto-map \\HOST to Z: from the W95TOOLS guest agent (#364)
* Auto-map \\HOST to Z: from W95TOOLS at login

W95TOOLS.EXE now calls WNetAddConnectionA("\\\\HOST\\HOST", NULL, "Z:")
on a short retry timer (5 tries, 3s apart) so the shared folder shows up
as a drive without a trip through Start -> Run. MPR.DLL is LoadLibrary'd
so the EXE keeps its USER32/KERNEL32-only import table and still launches
if MPR is somehow absent. Skipped if Z: is already taken; gives up
silently if no share is configured.

Works for any user folder because the SMB server's tree-connect already
routes every share name other than TOOLS/IPC$ to the user share; added a
comment in server.ts pointing at the dependency.

Verified by cold-booting the image with the new vs. old binary in
StartUp: new -> tree connect to \\HOST\\HOST within ~5s of desktop and
z:\ opens in Explorer; old -> no SMB traffic after 55s at desktop.

* Drop rebuilt W95TOOLS.EXE from the diff

Binary will be rebuilt and baked into the image alongside the next
default-state re-bake; keep this PR source-only.

* Stop tracking guest-tools/agent/W95TOOLS.EXE

It's a build output of `make -C guest-tools/agent` and CI doesn't
consume it (the disk image is baked out-of-band), so there's no reason
to carry the binary in git.
2026-04-12 09:16:54 -07:00
Felix Rieseberg
b74e6c7b0a Fix guest TCP recv() stalling when v86 NE2000 TX ring wraps (#363)
* Fix guest TCP recv() stalling under concurrent traffic

v86's fake_network stores TCPConnection routing fields (hsrc/hdest/
psrc/pdest) as zero-copy subarrays of the SYN frame, which is itself a
view into the NE2000 TX ring. Win95's driver uses a 12-slot ring; once
it wraps (any concurrent SMB/NBNS/ping while waiting for an upstream
reply), pump() emits segments with whatever IP now occupies that slot,
the guest RSTs them, and recv() blocks forever.

- libv86.js: copy the four address arrays at TCPConnection construction
  (matches felixrieseberg/v86@dd13099c on fake-network-copy-tcp-addrs,
  now merged into windows95-base)
- tools/probe-tcp.sh + net/tcp-trace.ts + tcp-relay.ts test stub +
  debug-harness WIN95_PROBE_RUN2: end-to-end regression harness
  (boot → ping -t → telnet → async write after ring wrap → assert ACK).
  All env-gated, no production-path change.
- docs/v86-patches.md: tracker for all fork patches + upstream PR state
- update-v86 SKILL.md: cross-link and new fork-branch entry

* Drop checked-in upstream PR description

Belongs on the GitHub PR, not in the repo.
2026-04-12 08:03:03 -07:00
Felix Rieseberg
3c63139fae Await restoreState before run(); update home.htm contact to Twitter
restoreState() was fire-and-forget so emulator.run() started cold-booting
while the snapshot was still being read and applied over live RAM/regs.
2026-04-11 20:29:52 -07:00
Felix Rieseberg
bc76e9c79a Shared text clipboard via VMware backdoor + W95TOOLS.EXE guest agent (#361)
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.
2026-04-11 20:17:06 -07:00
Felix Rieseberg
6e73df11ae Enable CD-ROM via a synchronous fs-backed buffer (#362)
v86's async loaders leave the ATAPI drive in BSY across an event-loop
turn after a READ(10) CDB. Win95's ESDI_506 reads status twice, sees
BSY both times, and issues DEVICE RESET ~165 instructions later, which
cancels the in-flight read — the drive enumerates but D: never mounts.

Serve the ISO through a small fs.readSync-backed buffer so the data is
available before the next emulated instruction runs, and re-enable the
CD-ROM settings tab.

Also: WIN95_PROBE_CDROM / WIN95_PROBE_CDTRACE harness hooks, and pump
one screen-adapter frame before screenshotting so probe captures work
when the Electron window is occluded.
2026-04-11 19:31:57 -07:00
Felix Rieseberg
5da7f94c5a Make "Start without state" always enabled (#360)
The menu item now works whether the machine is running or not. bootFromScratch() awaits stopEmulator() first (a no-op when already stopped), so invoking it mid-session saves the current state, tears down the running VM, and boots a fresh one.
2026-04-11 18:39:45 -07:00
Felix Rieseberg
766497bd5d v86: defer V86-mode VBE disable; add DOS-box probe scenario (#359)
Rebuilds libv86.js from felixrieseberg/v86@windows95-base, which now
carries vga-defer-vbe-disable-v86: when a windowed DOS VM's vgabios
writes dispi[4]=0, Win9x's VDD passes that through (it doesn't know
about ports 1CE/1CF) while virtualising the rest of the mode-set, so
v86 used to drop out of LFB rendering with the legacy registers still
holding SVGA values and the screen turned to planar garbage. The fix
defers the disable until a legacy attribute-mode write actually
reaches the hardware.

debug-harness: WIN95_PROBE_DOSBOX=1 opens command from Run, types
dir, optionally Alt+Enters (WIN95_PROBE_DOSBOX_ALTENTER=1).
WIN95_PROBE_VGATRACE=1 wraps the VGA io.ports[] entries (not the
VGAScreen methods, which are captured by-value at registration) and
dumps [port, op, value, eip+VM/PE/CPL] tuples to
/tmp/win95-vgatrace.json — that EIP/mode column is what pinned the
leak on V86-mode vgabios at C000:2C8x.
2026-04-11 17:32:07 -07:00
Felix Rieseberg
85c44513cb Raw TCP relay for non-port-80 guest egress + DoH DNS with magic-name shim (#358)
The fetch network adapter only intercepts port 80 (parsed as HTTP and
replayed via fetch). Everything else was RST'd. This adds a second
tcp-connection bus listener that bridges any other destination port
straight to a Node net.Socket in the renderer — no extra process, no
WebSocket relay, no new dependencies.

- net/tcp-relay.ts: hook v86 tcp-connection, accept SYN synchronously,
  open a real socket to conn.psrc:conn.sport, buffer until connect,
  pipe both directions, tear down on either side closing. Loopback,
  link-local and multicast are refused; the rest of RFC1918 is left
  reachable so the guest can still talk to LAN devices.
- net/dns-shim.ts: dns_method is now "doh" so the guest gets real IPs
  for the relay to dial, but Cloudflare can't answer single-label names
  like "windows95" or the NNN.external localhost magic. The shim wraps
  global fetch, spots /dns-query POSTs for those names, and answers
  with the same 192.168.87.1 placeholder the static resolver used to
  hand out — the port-80 fetch path then takes over via the Host header
  exactly as before.
- emulator.tsx: wire both in next to the SMB hook; set dns_method.
- debug-harness.ts: WIN95_PROBE_RUN/_RUN_AFTER/_RUN_WAIT to type an
  arbitrary command into Start→Run (used to e2e-test the relay with
  telnet against a host echo server).
2026-04-11 15:36:20 -07:00
Felix Rieseberg
ccd2b28169 Add "Boot from scratch" to the Machine menu (#357)
* Add "Boot from scratch" to the Machine menu

Wires a new MACHINE_BOOT_FROM_SCRATCH IPC command from the app menu to the existing Emulator.bootFromScratch() handler, so users can start a fresh boot without going through the settings card. Enabled only while the machine is stopped, matching Start.

* Rename menu item to "Start without state"

* Move "Send key" items below power management in Machine menu
2026-04-11 15:27:22 -07:00
Felix Rieseberg
27b9e0eb7a Seamless mouse (VMware backdoor + VBADOS) and fs-backed TOOLS share (#355)
* Seamless mouse via VMware backdoor + fs-backed TOOLS share

v86 (rebuilt libv86.js from fork branch vmware-mouse): new VMwareMouse
device on port 0x5658 implementing GETVERSION/ABSPOINTER_* fed by the
existing mouse-absolute bus event. Move-only packets are coalesced so
the guest cursor never falls more than one frame behind. Emits
vmware-absolute-mouse on the bus when the guest driver toggles mode.

Renderer: listens for that event, keeps the v86 mouse enabled without
pointer lock, drops the startup auto-capture, and hides the host cursor
over the canvas (.seamless-mouse) while the driver is active. Falls back
to click-to-capture when no driver is present.

SMB: TOOLS share is now backed by the bundled guest-tools/ directory
(subdirectories work) with the synthetic README.TXT/_MAPZ.BAT overlaid
at the root. resolve() routes by tid; SEARCH and FIND_FIRST2 share a
single listForSearch helper.

guest-tools/mouse-driver/: VBMOUSE.EXE + VBMOUSE.DRV from VBADOS
(Javier S. Pedro, GPLv2). Load the TSR from AUTOEXEC.BAT and set
mouse.drv=vbmouse.drv in SYSTEM.INI to enable seamless mouse.

Also: tsconfig rootDir "." for TS 6.0 (preserves dist/src/ layout).

* docs: windows95-base now includes vmware-abspointer
2026-04-11 14:15:20 -07:00
Felix Rieseberg
1dbb853fe6 SMB: filename safety + hidden attrs; drop http://my-computer host browser (#354)
SMB share:
- Mark dotfiles HIDDEN and host junk (.DS_Store, Thumbs.db, ...) HIDDEN+SYSTEM
  so Explorer hides them by default but View > Show all files still works.
- Guard DOS device names (CON/PRN/AUX/NUL/CLOCK$/COM1-9/LPT1-9) in both the
  long name and the generated 8.3 name; covers nul.tar.gz and 'con .txt' too.
- Replace trailing dot/space runs with '_' so names don't alias their stripped
  form; block DEL and C1 control bytes.
- Fix duplicate listing of sanitized names (sfnMap was mutated mid-iteration).
- Thread attrs through OPEN/QUERY/FIND replies via DirEntry.attr / hostAttrs().
- New [5d] section in test-standalone.ts; 55 tests pass.

http file server:
- Remove the http://my-computer/ host-filesystem browser now that SMB covers
  folder sharing. fileserver.ts now serves only static/www at http://windows95/
  with a sep-suffixed traversal guard.
- Delete page-directory-listing.ts, page-error.ts, encoding.ts, hide-files.ts
  and the three isFileServer* settings keys (and unused SettingsManager.delete).
- Point static/www/apps.htm at the SMB share instead.
2026-04-11 14:15:20 -07:00
Felix Rieseberg
9b217731f5 Replace Parcel 1 with Vite (#353)
Swap the unmaintained parcel-bundler@1.x for Vite's build API, called
from the same generateAssets hook. Output layout is unchanged
(dist/src/main/main.js + dist/static/index.html + dist/renderer.{js,css})
so no runtime path changes — __dirname-based asset lookup, loadFile, and
packagerConfig.ignore all keep working.

Renderer is built in lib/CJS mode with Node builtins + electron
externalized; a one-line banner aliases exports=module.exports since the
Electron <script> context provides module/require but not a bare exports
global. CSS (98.css + root.css) is now imported from app.tsx so Vite
emits a single renderer.css with fonts inlined.

Drops parcel-bundler and rimraf (vite-build clears dist/ itself).
~800ms full build.
2026-04-11 14:15:19 -07:00
Felix Rieseberg
148f8e4874 SMB: long filenames, basename-derived share name, synthetic TOOLS share (#352)
Negotiate NT LM 0.12 (17-word response, Capabilities=0) so Win95 switches
from CMD_SEARCH (8.3-only) to TRANS2/FIND_FIRST2. Implement info level
0x104 (FILE_BOTH_DIRECTORY_INFO) with the win9x quirks Samba carries:
honor SearchCount/MaxDataCount (VREDIR drops the session if exceeded),
4-byte-align entries and the trans2 reply param/data blocks, dir
EndOfFile=0, FileName null-terminated with the null counted in
FileNameLength, and ShortNameLength=0 (a UCS-2 ShortName in an OEM
session GPFs shell32 on the single-directory probe Explorer does when
entering a subfolder). Add TRANS2_QUERY_FS_INFO (0x105/1/2), SMB_COM_SEEK
and core SMB_COM_READ, which the redirector falls back to with no
CAP_LARGE_READX. Sanitize >0xFF / Windows-reserved chars in display names
so emoji folder names don't truncate to '<' and wedge Explorer.

The user share is now named after path.basename of the mounted folder
(LANMAN-safe, ≤12 chars, with TOOLS/IPC$ collision avoided). A second
purely-synthetic TOOLS share holds _MAPZ.BAT and README.TXT so they
don't clutter the user's directory; treeConnect routes by share name to
a TID and every path-resolving handler branches on it so TOOLS never
touches the host fs.

48 protocol tests; verified end-to-end in the emulator (browse \\HOST,
open both shares, list Downloads with 85+ mixed-name entries, navigate
subfolders, open files in Notepad).
2026-04-11 12:26:54 -07:00
Felix Rieseberg
43c025929b Show git branch in window title during development (#351)
When running via yarn start on a branch other than master/main, append
the branch name to the window title (e.g. "windows95 (my-feature)").
No-op in packaged builds.
2026-04-11 11:06:08 -07:00
Felix Rieseberg
3b62e1c9b5 Replace Less with native CSS (#350)
The stylesheets only used Less for variables and nesting, both of which
are now native CSS features supported by Electron 41's bundled Chromium.

- src/less/ -> src/css/, all .less files renamed to .css
- @win-* variables -> :root custom properties + var()
- // comments -> /* */
- Dropped unused @win-silver
- Removed less devDependency and the dead 'less' npm script
  (pointed at a non-existent tools/lessc.js)
2026-04-11 10:45:04 -07:00
Felix Rieseberg
e9ddfab65d Stabilize VM info bar width, add sparklines and Interface settings (#349)
* Stabilize VM info bar width and add sparkline mode

* Add Interface settings tab and switch info bar to N.N unit format
2026-04-11 09:45:08 -07:00
Felix Rieseberg
89c0a8575d docs: update-v86.js rewrite + SMB/v86/testing knowledge (#348)
* Rewrite update-v86.js for the current build pipeline

Both v86 fixes we've been carrying are now real branches on the fork
(PR #1540 electron-renderer-fs-loader, PR #1541 ide-shared-registers)
combined as felixrieseberg/v86:windows95-base. update-v86.js no longer
needs to patch sources at build time — it just builds whatever's checked
out and copies the result.

Gone: the fallback-to-copy.sh path, the skew-day check, the structural
regex patches for load_file/exportSymbol/fetch-bind, the phantom-slave
guard (both are in the branch), the --js-only flag. If you don't have
cargo/clang/java/closure, the script fails loudly — no silent fallbacks.

Added: sanity checks against the installed libv86.js for the invariants
our SMB integration and parcel-build shim depend on, so if upstream
changes something load-bearing we see it as a WARN at update time
instead of a runtime failure.

Tested end-to-end: 5/5 sanity checks, fresh boot SUCCESS in 32s.

* docs and skills: capture SMB/v86/testing knowledge from the session

- docs/smb-share.md: user-facing SMB integration overview (how to mount in
  Win95, what's implemented, what's not). Points at the protocol-level
  README inside src/renderer/smb/ for wire-level gotchas.
- .claude/skills/probe-win95: how to boot and test the VM without a human.
  Env vars, file locations, failure modes, the XT scancode keyboard trick,
  bisect rules of thumb.
- .claude/skills/update-v86: how to pull upstream v86 changes, what the
  five sanity-check WARNs mean, how to retire the fork branches when the
  PRs merge upstream.

.gitignore narrowed to exclude only the runtime dirs (scheduled_tasks.lock,
worktrees) instead of the whole .claude/ tree, so skills can be committed.
2026-04-11 09:33:51 -07:00
Felix Rieseberg
2ef06eb412 Merge pull request #346 from felixrieseberg/claude/crazy-kilby
Modernize GitHub Actions workflow
2026-04-11 09:32:54 -07:00
Felix Rieseberg
3eab26fed1 check-links: allow 403 from github user-attachments CDN 2026-04-11 09:31:38 -07:00
Felix Rieseberg
1bef3cce62 Switch CI from yarn to npm (repo uses package-lock.json) 2026-04-11 09:04:03 -07:00
Felix Rieseberg
17e0182ecb Merge pull request #347 from felixrieseberg/claude/sharp-shirley
Redesign launcher UI with 98.css
2026-04-11 09:03:14 -07:00
Felix Rieseberg
20f7f8c70e Modernize GitHub Actions workflow
- Bump actions to latest releases and pin to commit SHAs
- Replace deprecated ::set-output + actions/cache with setup-node built-in yarn cache
- Bump Node 18 -> 20
- Use --frozen-lockfile in build job install
2026-04-11 08:17:18 -07:00
81 changed files with 5942 additions and 13181 deletions

View File

@@ -0,0 +1,122 @@
---
name: probe-win95
description: Boot Windows 95 in Electron under Claude's control, without a human clicking anything. Use when testing v86 updates, SMB changes, keyboard input, boot stability, or bisecting regressions.
---
# Probing Windows 95 autonomously
You can run and test the Win95 VM yourself. The harness is already wired
up — three pieces:
| File | Role |
|---|---|
| `src/renderer/debug-harness.ts` | Activated by `WIN95_PROBE=1`. Boots fresh automatically, samples CPU + VGA + text screen every 5s, writes `/tmp/win95-probe.json` + `/tmp/win95-screen.png`, detects SUCCESS vs FAIL modes, optionally drives keyboard input. |
| `src/renderer/smb/index.ts` | Wraps `console.log` so `[smb]` and `[nbns]` lines tee to `$TMPDIR/windows95-smb.log` (outside Electron, readable by any polling script — no CDP needed). |
| `tools/probe-boot.sh` | One-shot: kill leftovers → parcel build → launch Electron → poll `/tmp/win95-probe.done` → report → kill. |
## One-shot boot test
```sh
tools/probe-boot.sh
```
Prints SUCCESS or a FAIL verdict. ~40s on a clean run.
## Boot + type into Run
```sh
pkill -9 -f "windows95.*electron"; sleep 2
rm -f "$HOME/Library/Application Support/windows95/state-v4.bin"
rm -f /tmp/win95-probe.json /tmp/win95-probe.done \
"$TMPDIR/windows95-smb.log"
WIN95_PROBE=1 \
WIN95_PROBE_SCRIPT='HOST/HOST' \
WIN95_SMB_SHARE="$HOME/Downloads" \
./node_modules/.bin/electron . > /tmp/win95-electron.log 2>&1 &
```
`WIN95_PROBE_SCRIPT='HOST/HOST'` types `\\HOST\HOST` into Start → Run on
desktop. `WIN95_PROBE_DOSBOX=1` instead opens `command`, types `dir`,
and (with `WIN95_PROBE_DOSBOX_ALTENTER=1`) toggles fullscreen — this is
the regression scenario for the windowed-DOS-box VBE leak.
`WIN95_PROBE_CDROM=/path/to.iso` mounts an ISO on the secondary-IDE
ATAPI drive (bypasses the settings UI). `WIN95_PROBE_CDTRACE=1` logs
every secondary-channel ATA/ATAPI command to `/tmp/win95-cdtrace.log`.
`WIN95_PROBE_VGATRACE=1` wraps the VGA I/O ports at the `io.ports[]`
layer and writes `[port, op, value, "eip VMPE cplN"]` tuples to
`/tmp/win95-vgatrace.json` every tick (heavy — can hit 1M entries during
boot). `/``\` substitution (env var / shell quoting, pragmatism). The
harness drives it via XT scancodes — Win95 doesn't have Win+R (Win98+
only), so the sequence is Esc, Esc, Ctrl+Esc, R, backslashes + text,
Enter.
## Reading results
| File | What |
|---|---|
| `/tmp/win95-probe.json` | Live status: `phase` (`init`/`text-mode`/`splash`/`desktop`), `gfxW/H`, `textScreen`, `instructionDelta`, `verdict` |
| `/tmp/win95-probe.done` | Written once when verdict is decided |
| `/tmp/win95-screen.png` | Canvas screenshot, refreshed each tick |
| `$TMPDIR/windows95-smb.log` | SMB/NBNS protocol trace |
| `/tmp/win95-electron.log` | Electron stderr |
## Verdicts
| Verdict | Meaning | Action |
|---|---|---|
| `SUCCESS` | Canvas ≥640×480, CPU active, uptime >30s | desktop reached |
| `FAIL_VXDLINK` | "Invalid VxD dynamic link call" | flaky — retry |
| `FAIL_IOS` / `FAIL_PROTECTION` | IOS subsystem protection error | usually driver/BIOS mismatch |
| `FAIL_KRNL386` | "Cannot find KRNL386.EXE" in safe mode | disk reads returning garbage — wasm/BIOS drift |
| `FAIL_SPLASH_HANG` | Canvas stuck 320×400 for >70s | IRQ starvation — if you're on v86 master, check the IDE register fix |
| `FAIL_HUNG` | CPU stopped advancing or text screen frozen 40s | hard hang |
## Rules of the road
- **Sporadic bluescreens are normal** on all v86 versions. One FAIL_VXDLINK
or FAIL_HUNG doesn't prove anything — retry up to 3×.
- **Always clean state** (`state-v4.bin`) before a probe. `pkill` on a
wedged Electron triggers `onbeforeunload`, saving the *corrupted* state.
Deleting it forces fallback to `images/default-state.bin`.
- **Don't trust the text buffer in graphics mode.** After desktop (≥640×480)
the stale BIOS text lingers in the buffer. The harness's `phase` field
accounts for this; don't re-read `textScreen` in a `desktop` phase and
think you hit a BSOD.
- **Kill Electron when done.** Background processes pile up, each holding
the disk image lock. `pkill -f "windows95.*electron"` on every path out.
## Bisecting v86
`tools/bisect-v86.sh <commit>` handles one step. The harness retries 3×
per commit. Hard-won lessons:
1. **Validate bounds against a known-good binary.** Source-built wasm can
drift from prod due to cargo/rustc version differences. We hit this:
the "GOOD" bound produced a wasm that couldn't read the disk at all.
2. **JS-only when toolchain drifts.** Keep the prod wasm, rebuild only
libv86.js at each commit. Closure is deterministic enough; cargo
isn't always. Works until you cross a commit that changes the JS↔wasm
ABI (for v86, the APIC→Rust port in Aug 2025).
3. **Retry on FAIL, never on SUCCESS.** One SUCCESS = commit is good.
Three different FAILs at the same commit = commit is bad.
4. **State cleanup between runs** (see above). Skipping this is the #1
cause of spurious "bad" verdicts during bisect.
## Extending the harness
- New verdicts: add to the chain in `collectStatus` in `debug-harness.ts`
- New keyboard actions: extend `runScript` (current types: `keys`, `chord`,
`text`, `wait`)
- New probe signals: add to `ProbeStatus` interface
Gate everything new on `process.env.WIN95_PROBE === "1"` so it stays out
of the normal app.
## Common failure diagnostics
| Symptom | Check |
|---|---|
| No SMB traffic at all | `$TMPDIR/windows95-smb.log` should have `hooked adapter` line. If absent, v86 API changed — see `src/renderer/smb/README.md` |
| SMB hooks fire, no connection | Win95's "NetBIOS over TCP/IP" checkbox — bake into default-state.bin |
| Boot hangs on `2996c087` or older v86 | You probably have a ABI-mismatched wasm/JS pair. Prod wasm is the ground truth; rebuild JS against it. |

View File

@@ -0,0 +1,151 @@
---
name: update-v86
description: Build and install v86 (wasm + libv86.js + BIOS) into windows95. Use when pulling upstream v86 changes, fixing a broken build, verifying the fork branches are still in sync, or setting up a fresh v86 checkout.
---
# Updating v86
windows95 builds v86 from source — not from copy.sh. Two small bugfix
patches ride along on a fork branch until the upstream PRs land.
## Sources
| File | Built from |
|---|---|
| `src/renderer/lib/libv86.js` | `make build/libv86.js` in `../v86` |
| `src/renderer/lib/build/v86.wasm` | `make build/v86.wasm` |
| `bios/seabios.bin`, `bios/vgabios.bin` | copied from `../v86/bios/` |
`tools/update-v86.js` runs those targets, copies the artifacts, runs 5
sanity checks, and fails loudly if any prerequisite is missing. No
fallbacks, no fetching from copy.sh.
## The fork branch
v86 should be checked out on **`felixrieseberg/v86:windows95-base`**.
That branch merges the feature branches tracked in
[`docs/v86-patches.md`](../../../docs/v86-patches.md) — keep that table
in sync with this list. Each is upstreamable on its own:
- **`electron-renderer-fs-loader`** (PR #1540) — `src/lib.js` uses
`require("fs")` instead of `await import("node:fs/promises")`. Dynamic
import of `node:` URLs doesn't work in an Electron renderer.
- **`ide-shared-registers`** (PR #1541) — `src/ide.js` writes ATA Command
Block registers (Features, Sector Count, LBA Low/Mid/High) to both
master and slave. Without this, Win95/98 hang at the splash screen on
any disk >~535MiB. Root cause: v86 commit `1b90d2e7` changed those
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
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, and the legacy text-clipboard commands
69 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`.
- **`fake-network-copy-tcp-addrs`** — `src/browser/fake_network.js`
copies the four address subarrays (`hsrc/hdest/psrc/pdest`) when a
`TCPConnection` is created from an inbound SYN. Upstream stores them
as zero-copy views into the NE2000 TX ring; once the guest's 12-slot
TX ring wraps (any concurrent traffic — SMB, NBNS, ping), `pump()`
builds segments with whatever IP now occupies that slot, the guest
RSTs them as belonging to no TCB, and `recv()` blocks forever.
Exercised by `tools/probe-tcp.sh`.
- **`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 3B03DF for a windowed DOS VM
but not 1CE/1CF, so vgabios's VBE-disable leaks through while the rest
of its mode-set is captured into the VM's virtual register file —
without this the screen turns to planar garbage the moment you open a
DOS box.
## Prerequisites
```sh
rustup target add wasm32-unknown-unknown
brew install openjdk
# one-time: fetch the Closure compiler v86's Makefile pins to
curl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar \
-o ../v86/closure-compiler/compiler.jar
```
Closure **must** be v20210601 — newer versions hit
[closure-compiler#3972](https://github.com/google/closure-compiler/issues/3972)
on v86's source. The pin is in v86's Makefile.
## Steps
```sh
cd ../v86
git fetch fork origin
git checkout windows95-base
git rebase fork/windows95-base # in case fork was updated elsewhere
cd ../windows95
node tools/update-v86.js
```
That's it. Script runs both `make` targets, copies, verifies.
## Sanity-check WARNs
The 5 checks assert invariants `src/renderer/smb/index.ts` and
`tools/parcel-build.js` depend on. A WARN means upstream changed
something load-bearing — don't ignore it:
1. **`await import("node:...")` still present** → PR #1540 was reverted
or the pattern moved. Electron renderer will fail to load disk images.
2. **`master.features_reg=` missing in minified** → PR #1541 was reverted
or `windows95-base` lost the commit. Win95 will hang at splash on
disks >535MiB. Check `cd ../v86 && git log --oneline windows95-base`.
3. **Export pattern changed**`tools/parcel-build.js` shim needs
updating. Look for `module.exports.V86=` and `window.V86=`.
4. **`tcp-connection` event gone** → SMB falls back to the old-API theft
hack in `src/renderer/smb/index.ts` — still works, but surprising.
5. **`on_tcp_connection` gone** → old-API fallback is dead. SMB integration
only works via the `tcp-connection` bus event now. Harmless; update
the comment in `index.ts` and retire the theft code.
## After updating, probe-test
```sh
node tools/update-v86.js && tools/probe-boot.sh
```
Should land SUCCESS in ~40s. If FAIL_SPLASH_HANG, the IDE fix didn't
take — check `grep master.features_reg src/renderer/lib/libv86.js`. If
FAIL_VXDLINK, retry — sporadic bluescreens are normal (see the
`probe-win95` skill).
## When a PR merges upstream
Rebase `windows95-base` to drop the now-redundant commit:
```sh
cd ../v86
git fetch origin
git checkout windows95-base
git rebase origin/master # drops the merged commit cleanly
git push fork windows95-base --force-with-lease
```
If **both** PRs are upstream, retire the fork branch entirely:
1. Point `tools/update-v86.js` default at `origin/master` (it already
uses `../v86`, so just `git checkout master` there)
2. Delete `fork/windows95-base`
3. Remove this skill's "The fork branch" section
4. Confirm the 5 sanity checks still pass — they're version-agnostic
## Integration contract with SMB
The SMB server sits on top of v86's network adapter. Details in
`src/renderer/smb/README.md`. Short version: the new path uses the
`tcp-connection` bus event; the fallback path uses
`adapter.on_tcp_connection` callback + connection-theft (stealing a
`TCPConnection` the HTTP probe builds for us). Both use `.on_data` on
the conn, not `.on("data")`, because Closure dead-code-eliminates the
event emitter plumbing.
If any v86 update breaks these assumptions, `src/renderer/smb/index.ts`
needs updating, not just `tools/update-v86.js`.

View File

@@ -12,30 +12,22 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: actions/setup-node@v1
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 18.x
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
node-version: 20.x
cache: npm
- name: Install
run: yarn --frozen-lockfile
run: npm ci
- name: lint
run: yarn lint
run: npm run lint
build:
needs: lint
name: Build (${{ matrix.os }} - ${{ matrix.arch }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Build for supported platforms
# https://github.com/electron/electron-packager/blob/ebcbd439ff3e0f6f92fa880ff28a8670a9bcf2ab/src/targets.js#L9
@@ -55,59 +47,56 @@ jobs:
arch: arm64
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: actions/setup-node@v1
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 18.x
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
if: matrix.os != 'macOS-latest'
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
node-version: 20.x
cache: npm
- name: Set MacOS signing certs
if: matrix.os == 'macOS-latest'
run: chmod +x tools/add-macos-cert.sh && ./tools/add-macos-cert.sh
env:
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
- name: Set Windows signing certificate
if: matrix.os == 'windows-latest'
continue-on-error: true
id: write_file
uses: timheuer/base64-to-file@v1
with:
fileName: 'win-certificate.pfx'
encodedString: ${{ secrets.WINDOWS_CODESIGN_P12 }}
- name: Set up Azure Trusted Signing
if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
shell: pwsh
run: |
nuget install Microsoft.Trusted.Signing.Client -Version 1.0.60 -OutputDirectory . -NonInteractive
$signtool = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter signtool.exe | Where-Object { $_.FullName -like "*\x64\*" } | Sort-Object FullName -Descending | Select-Object -First 1
echo "SIGNTOOL_PATH=$($signtool.FullName)" >> $env:GITHUB_ENV
echo "AZURE_CODE_SIGNING_DLIB=$((Resolve-Path 'Microsoft.Trusted.Signing.Client.1.0.60/bin/x64/Azure.CodeSigning.Dlib.dll').Path)" >> $env:GITHUB_ENV
- name: Download disk image (ps1)
run: tools/download-disk.ps1
if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
env:
DISK_URL: ${{ secrets.DISK_URL }}
DISK_REPO: ${{ vars.DISK_REPO }}
DISK_TAG: ${{ vars.DISK_TAG }}
GH_TOKEN: ${{ secrets.IMAGES_REPO_TOKEN }}
- name: Download disk image (sh)
run: chmod +x tools/download-disk.sh && ./tools/download-disk.sh
if: matrix.os != 'windows-latest' && startsWith(github.ref, 'refs/tags/')
env:
DISK_URL: ${{ secrets.DISK_URL }}
DISK_REPO: ${{ vars.DISK_REPO }}
DISK_TAG: ${{ vars.DISK_TAG }}
GH_TOKEN: ${{ secrets.IMAGES_REPO_TOKEN }}
- name: Install
run: yarn
run: npm ci
- name: Make
if: startsWith(github.ref, 'refs/tags/')
run: yarn make --arch=${{ matrix.arch }}
run: npm run make -- --arch=${{ matrix.arch }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WINDOWS_CODESIGN_FILE: ${{ steps.write_file.outputs.filePath }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_CODE_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_CODE_SIGNING_ACCOUNT_NAME }}
AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME }}
- name: Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -118,4 +107,4 @@ jobs:
out/**/*.dmg
out/**/*setup*.exe
out/**/*.rpm
out/**/*.zip
out/**/*.zip

5
.gitignore vendored
View File

@@ -15,4 +15,7 @@ trusted-signing-metadata.json
.env
electron-windows-sign.log
.npmrc
/.claude/
guest-tools/**/*.EXE
guest-tools/**/*.exe
/.claude/scheduled_tasks.lock
/.claude/worktrees/

View File

@@ -15,25 +15,18 @@ This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes,
</td>
<td>
<span>32-bit</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-setup-ia32.exe">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-5.0.0-setup-ia32.exe">
💿 Installer
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-win32-ia32-4.0.0.zip">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-win32-ia32-5.0.0.zip">
📦 Standalone Zip
</a>
<br />
<span>64-bit</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-setup-x64.exe">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-5.0.0-setup-x64.exe">
💿 Installer
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-win32-x64-4.0.0.zip">
📦 Standalone Zip
</a><br />
<span>ARM64</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-setup-arm64.exe">
💿 Installer
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-win32-arm64-4.0.0.zip">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-win32-x64-5.0.0.zip">
📦 Standalone Zip
</a><br />
<span>
@@ -48,11 +41,11 @@ This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes,
</td>
<td>
<span>Apple Silicon Processor</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-darwin-arm64-4.0.0.zip">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-darwin-arm64-5.0.0.zip">
📦 Standalone Zip
</a><br />
<span>Intel Processor</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-darwin-x64-4.0.0.zip">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-darwin-x64-5.0.0.zip">
📦 Standalone Zip
</a>
<span>
@@ -67,10 +60,24 @@ This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes,
</td>
<td>
<span>64-bit</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-1.x86_64.rpm">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-5.0.0-1.x86_64.rpm">
💿 rpm
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95_4.0.0_amd64.deb">
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95_5.0.0_amd64.deb">
💿 deb
</a><br />
<span>ARM64</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-5.0.0-1.arm64.rpm">
💿 rpm
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95_5.0.0_arm64.deb">
💿 deb
</a><br />
<span>ARM</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95-5.0.0-1.armv7hl.rpm">
💿 rpm
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.0/windows95_5.0.0_armhf.deb">
💿 deb
</a><br />
</td>

45
docs/smb-share.md Normal file
View File

@@ -0,0 +1,45 @@
# Host folder over SMB
Windows 95 can mount a host folder as a network drive. The server lives in
`src/renderer/smb/` — ~1500 lines, zero dependencies, read-only. Defaults to
`~/Downloads`, configurable in Settings.
## Inside Win95
Open **My Computer****Z:**. The `W95TOOLS.EXE` guest agent auto-maps
`\\HOST\HOST` to `Z:` at login, so the share is available as a drive letter
out of the box. If the agent isn't running, the underlying UNC path still
works via Start → Run → `\\HOST\HOST`.
NetBIOS over TCP/IP must be enabled (Control Panel → Network → TCP/IP
properties → NetBIOS tab). This is baked into the default state image.
## Architecture
One SMB session per `TCPConnection`, hooked off v86's network adapter. The
server speaks SMB1 (LANMAN2.1 dialect) because that's what Win95 negotiates.
Full breakdown in `src/renderer/smb/README.md` — that file has the protocol
gotchas learned during implementation (NT dialect trap, NetBIOS name
null-termination, 8.3 `~N` mapping, RAP descriptor parsing).
**Security:** read-only, symlink-aware path traversal guard, share path
validated in main-process IPC. Not exposed until `smbSharePath` is set in
settings or `WIN95_SMB_SHARE=...` is in the env.
## Tests
```sh
npx tsc --ignoreConfig --module commonjs --target es2020 --esModuleInterop \
--moduleResolution bundler --outDir /tmp/smb-test --skipLibCheck \
src/renderer/smb/*.ts && node /tmp/smb-test/test-standalone.js
```
35 protocol tests, full round-trips with real file I/O. No Electron needed.
## What's not implemented
- Writes (read-only by design, but OPEN is easy to extend)
- Long filenames via TRANS2 (we serve 8.3 through the legacy SEARCH path,
which is enough for Win95 Explorer but loses the original casing/length)
- Multiple shares — everything is one share named `HOST`
- Authentication — guest access only

25
docs/v86-patches.md Normal file
View File

@@ -0,0 +1,25 @@
# v86 patches carried by windows95
windows95 builds v86 from a fork branch rather than upstream master.
Each fix lives on its own feature branch in `felixrieseberg/v86`, has an
upstream PR against `copy/v86`, and is merged into the integration
branch `felixrieseberg/v86:windows95-base` (which is what
`tools/update-v86.js` builds from). When a PR is merged upstream, drop
its row here, delete the feature branch, and rebase `windows95-base`.
| Fix | Branch | Upstream PR | windows95-base | Why we need it |
|---|---|---|---|---|
| Node fs loader in Electron renderer | [`electron-renderer-fs-loader`](https://github.com/felixrieseberg/v86/tree/electron-renderer-fs-loader) | [copy/v86#1540](https://github.com/copy/v86/pull/1540) | ✅ | `await import("node:fs/promises")` fails in an Electron renderer; disk images don't load. |
| IDE shared Command Block registers | [`ide-shared-registers`](https://github.com/felixrieseberg/v86/tree/ide-shared-registers) | [copy/v86#1541](https://github.com/copy/v86/pull/1541) | ✅ | Win9x hangs at splash on disks >535 MiB because per-device register writes violate the ATA shared-register-file spec. |
| VMware absolute-pointer backdoor | [`vmware-abspointer`](https://github.com/felixrieseberg/v86/tree/vmware-abspointer) | [copy/v86#1542](https://github.com/copy/v86/pull/1542) | ✅ | Port 0x5658 GETVERSION + ABSPOINTER_* so VBMOUSE can track the host cursor 1:1 without pointer lock. |
| VMware text-clipboard backdoor | [`vmware-clipboard`](https://github.com/felixrieseberg/v86/tree/vmware-clipboard) | — *(stacked on #1542)* | ✅ | Legacy backdoor commands 69 so `W95TOOLS.EXE` can sync `CF_TEXT` with the host. |
| Defer V86-mode VBE disable | [`vga-defer-vbe-disable-v86`](https://github.com/felixrieseberg/v86/tree/vga-defer-vbe-disable-v86) | [copy/v86#1543](https://github.com/copy/v86/pull/1543) | ✅ | Opening a windowed DOS box leaks vgabios's VBE-disable past Win9x's VDD and turns the screen to planar garbage. |
| fake_network: copy TCP addrs | [`fake-network-copy-tcp-addrs`](https://github.com/felixrieseberg/v86/tree/fake-network-copy-tcp-addrs) | — | ✅ | `TCPConnection` routing fields alias the NE2000 TX ring; concurrent guest traffic retargets async replies and the guest RSTs them. |
## Adding a fix
1. Branch off `origin/master` in `../v86`, commit, push to `fork`.
2. Open the PR against `copy/v86`.
3. `git checkout windows95-base && git merge --no-ff <branch> && git push fork windows95-base`
4. `node tools/update-v86.js` in this repo to rebuild `libv86.js` / `v86.wasm`.
5. Add a row above and a bullet in `.claude/skills/update-v86/SKILL.md`.

View File

@@ -4,8 +4,6 @@ const package = require('./package.json');
require('dotenv').config()
process.env.TEMP = process.env.TMP = `C:\\Users\\FelixRieseberg\\AppData\\Local\\Temp`
const FLAGS = {
SIGNTOOL_PATH: process.env.SIGNTOOL_PATH,
AZURE_CODE_SIGNING_DLIB: process.env.AZURE_CODE_SIGNING_DLIB || path.join(__dirname, 'Microsoft.Trusted.Signing.Client.1.0.60/bin/x64/Azure.CodeSigning.Dlib.dll'),
@@ -17,17 +15,22 @@ const FLAGS = {
APPLE_ID_PASSWORD: process.env.APPLE_ID_PASSWORD,
}
fs.writeFileSync(FLAGS.AZURE_METADATA_JSON, JSON.stringify({
Endpoint: process.env.AZURE_CODE_SIGNING_ENDPOINT || "https://wcus.codesigning.azure.net",
CodeSigningAccountName: process.env.AZURE_CODE_SIGNING_ACCOUNT_NAME,
CertificateProfileName: process.env.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME,
}, null, 2));
let windowsSign;
if (FLAGS.AZURE_TENANT_ID && FLAGS.SIGNTOOL_PATH) {
fs.writeFileSync(FLAGS.AZURE_METADATA_JSON, JSON.stringify({
Endpoint: process.env.AZURE_CODE_SIGNING_ENDPOINT || "https://wcus.codesigning.azure.net",
CodeSigningAccountName: process.env.AZURE_CODE_SIGNING_ACCOUNT_NAME,
CertificateProfileName: process.env.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME,
}, null, 2));
const windowsSign = {
signToolPath: FLAGS.SIGNTOOL_PATH,
signWithParams: `/v /dlib ${FLAGS.AZURE_CODE_SIGNING_DLIB} /dmdf ${FLAGS.AZURE_METADATA_JSON}`,
timestampServer: "http://timestamp.acs.microsoft.com",
hashes: ["sha256"],
windowsSign = {
signToolPath: FLAGS.SIGNTOOL_PATH,
signWithParams: `/v /dlib ${FLAGS.AZURE_CODE_SIGNING_DLIB} /dmdf ${FLAGS.AZURE_METADATA_JSON}`,
timestampServer: "http://timestamp.acs.microsoft.com",
hashes: ["sha256"],
};
} else {
console.warn('AZURE_TENANT_ID / SIGNTOOL_PATH not set; Windows binaries will not be signed');
}
module.exports = {

45
guest-tools/README.md Normal file
View File

@@ -0,0 +1,45 @@
# guest-tools
Files and folders in this directory are exposed read-only inside the VM at
`\\HOST\TOOLS` (alongside the synthetic `_MAPZ.BAT` and `README.TXT`).
Drop drivers and utilities here that you want available from within
Windows 95.
## mouse-driver/ — seamless mouse (VBADOS)
`VBMOUSE.EXE` (DOS TSR) + `VBMOUSE.DRV` (Windows 3.x/9x driver) from
[VBADOS](https://git.javispedro.com/cgit/vbados.git/) by Javier S. Pedro,
GPLv2. Talks to v86's VMware mouse backdoor (port 0x5658) so the Windows
95 cursor tracks the host cursor pixel-for-pixel without pointer lock.
Install inside the guest:
1. Copy `\\HOST\TOOLS\mouse-driver\VBMOUSE.EXE` to `C:\` and add a
`C:\VBMOUSE.EXE` line to `C:\AUTOEXEC.BAT`.
2. Windows Setup (or Control Panel → Mouse → General → Change → Have
Disk) → browse to `\\HOST\TOOLS\mouse-driver` → pick **VBMouse int33
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 bridges Windows 95's
`CF_TEXT` clipboard to the host (legacy backdoor commands 69; host side
is `src/renderer/clipboard.ts`, which polls Electron's clipboard) and
auto-maps `\\HOST\HOST` to `Z:` at login via `WNetAddConnection`, so the
shared folder shows up as a drive without a trip through Start → Run.
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).

View File

@@ -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

View File

@@ -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' <w95tools.c >$@
clean:
rm -f W95TOOLS.EXE

View File

@@ -0,0 +1,222 @@
/*
* W95TOOLS — guest-side integration agent for the windows95 emulator.
*
* Currently: bidirectional text clipboard, and auto-mapping the host's
* SMB share to Z: at login. 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 <windows.h>
#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
#define TIMER_CLIP 1
#define TIMER_MAP 2
#define MAP_TRIES 5
#define MAP_DELAY 3000
/* The host SMB server routes any share name other than TOOLS/IPC$ to the
* user's folder, so a fixed UNC works regardless of which directory they
* picked. */
#define MAP_DRIVE "Z:"
#define MAP_UNC "\\\\HOST\\HOST"
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 int g_map_tries;
/* Map \\HOST to Z:. Loaded lazily so we don't grow a link-time dep on MPR;
* if the call fails (no share configured, network not up yet) the caller
* retries a few times on a timer and then gives up silently. */
static int map_host_drive(void)
{
typedef DWORD (APIENTRY *WNetAddConn)(LPCSTR, LPCSTR, LPCSTR);
static WNetAddConn fn;
DWORD rc;
if (!fn) {
HMODULE mpr = LoadLibrary("MPR.DLL");
if (!mpr) return 1;
fn = (WNetAddConn)GetProcAddress(mpr, "WNetAddConnectionA");
if (!fn) return 1;
}
if (GetDriveType(MAP_DRIVE "\\") > 1) return 1; /* letter taken */
rc = fn(MAP_UNC, 0, MAP_DRIVE);
return rc == NO_ERROR;
}
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, TIMER_CLIP, POLL_MS, 0);
SetTimer(hwnd, TIMER_MAP, 1, 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:
if (wp == TIMER_MAP) {
if (map_host_drive() || ++g_map_tries >= MAP_TRIES)
KillTimer(hwnd, TIMER_MAP);
else
SetTimer(hwnd, TIMER_MAP, MAP_DELAY, 0);
return 0;
}
pull_from_host(hwnd);
return 0;
case WM_DESTROY:
ChangeClipboardChain(hwnd, g_next);
KillTimer(hwnd, TIMER_CLIP);
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;
}

View File

@@ -0,0 +1,6 @@
VBMouse - DOS/Windows absolute mouse driver
Copyright (C) 2022 Javier S. Pedro
Licensed under the GNU General Public License, version 2 or later.
Source: https://git.javispedro.com/cgit/vbados.git/
Binaries extracted from https://depot.javispedro.com/vbox/vbados/vbados.flp

View File

@@ -0,0 +1,8 @@
[data]
Version = "3.0"
[disks]
1 =., "VBADOS driver disk", disk1
[pointing.device]
vbmouse = 1:vbmouse.drv, "VBMouse int33 absolute mouse driver", x:*vmd, vbmouse

Binary file not shown.

View File

@@ -0,0 +1,54 @@
# Language: English
# Codepage: 437
#
# Spaces before text must be kept. Be sure that no spaces are
# added to the end of the lines.
#
0.0:Usage:
0.1: VBMOUSE <ACTION> <ARGS..>
0.2:Supported actions and options:
0.3: install Install the driver (default).
0.4: low Install in conventional memory (otherwise UMB).
0.5: uninstall Uninstall the driver from memory.
0.6: wheel <ON|OFF> Enable/disable wheel API support.
0.7: wheelkey <KEY|OFF> Emulate a specific keystroke on wheel scroll.
0.8: Supported keys: updn, pageupdn.
0.9: integ <ON|OFF> Enable/disable VirtualBox integration.
0.10: hostcur <ON|OFF> Enable/disable mouse cursor rendering in the host.
0.11: reset Reset mouse driver.
1.0:Wheel mouse found and enabled\n
1.1:Setting wheel support to %s\n
1.2:enabled
1.3:disabled
1.4:Generate Up Arrow / Down Arrow key presses on wheel movement\n
1.5:Generate PageUp / PageDown key presses on wheel movement\n
1.6:Disabling wheel keystroke generation\n
1.7:VirtualBox integration enabled\n
1.8:Disabled VirtualBox integration\n
1.9:VirtualBox integration already disabled or not available\n
1.10:Setting host cursor to %s\n
1.11:Found VMware protocol version %ld\n
1.12:VMware integration enabled\n
1.13:Disabled VMware integration\n
1.14:VMware integration already disabled or not available\n
1.15:Neither VirtualBox nor VMware integration available\n
1.16:VirtualBox integration not available\n
1.17:Driver installed\n
1.18:Driver uninstalled\n
1.19:Reset mouse driver\n
1.20:\nVBMouse %x.%x (like MSMOUSE %x.%x)\n
1.21:VBMouse already installed\n
3.0:Could not find PS/2 wheel mouse\n
3.1:Wheel not detected or support not enabled\n
3.2:Unknown key '%s'\n
3.3:Cannot find VirtualBox PCI device, err=%d\n
3.4:Cannot lock buffer used for VirtualBox communication, err=%d\n
3.5:VirtualBox communication is not working, err=%d\n
3.6:Could not detect VMware, err=%ld\n
3.7:VMware absolute pointer error, err=0x%lx\n
3.8:Cannot init PS/2 mouse BIOS, err=%d\n
3.9:INT33 has been hooked by someone else, cannot safely remove\n
3.10:INT2F has been hooked by someone else, cannot safely remove\n
3.11:Driver data not found (driver not installed?)\n
3.12:Invalid argument '%s'\n
3.13:Argument required for '%s'\n

14296
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,15 @@
{
"name": "windows95",
"productName": "windows95",
"version": "4.0.0",
"version": "5.0.0",
"description": "Windows 95, in an app. I'm sorry.",
"main": "./dist/src/main/main.js",
"scripts": {
"start": "rimraf ./dist && electron-forge start",
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "prettier --write src/**/*.{ts,tsx} && npm run check-links",
"less": "node ./tools/lessc.js",
"tsc": "tsc -p tsconfig.json --noEmit",
"check-links": "node tools/check-links.js",
"postinstall": "patch-package"
@@ -28,23 +27,21 @@
"update-electron-app": "^3.1.2"
},
"devDependencies": {
"@electron-forge/cli": "7.8.3",
"@electron-forge/maker-deb": "7.8.3",
"@electron-forge/maker-flatpak": "^7.8.3",
"@electron-forge/maker-rpm": "^7.8.3",
"@electron-forge/maker-squirrel": "^7.8.3",
"@electron-forge/maker-zip": "^7.8.3",
"@electron-forge/publisher-github": "^7.8.3",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-flatpak": "7.11.1",
"@electron-forge/maker-rpm": "7.11.1",
"@electron-forge/maker-squirrel": "7.11.1",
"@electron-forge/maker-zip": "7.11.1",
"@electron-forge/publisher-github": "7.11.1",
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"dotenv": "^17.3.1",
"electron": "41.2.0",
"less": "^4.6.4",
"parcel-bundler": "^1.12.5",
"patch-package": "^8.0.1",
"prettier": "^3.8.1",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
"typescript": "^6.0.2",
"vite": "^7.3.1"
}
}

View File

@@ -13,8 +13,8 @@ index d318f6c..bfde740 100644
+ // await (0, resedit_1.resedit)(this.electronBinaryPath, resOpts);
+
+ const { spawnSync } = require('child_process');
+ const resEditProcess = spawnSync('node', [
+ 'C:\\Users\\FelixRieseberg\\Code\\windows95\\tools\\resedit.js',
+ const resEditProcess = spawnSync(process.execPath, [
+ require('path').resolve(process.cwd(), 'tools', 'resedit.js'),
+ this.electronBinaryPath
+ ], {
+ stdio: 'inherit'

View File

@@ -2,11 +2,22 @@ import * as path from "path";
const IMAGES_PATH = path.join(__dirname, "../../images");
// Bump when a release ships a v86/hardware/disk-image change that can't load
// older state-vN.bin snapshots. The app will detect an orphaned older state
// and offer to export the user's old C:\ as a mountable .img.
//
// That export splices the state's dirty-block overlay onto the *current*
// windows95.img — which only works while the partition table and FAT geometry
// stay constant across releases. If you ever resize the disk or reformat with
// different cluster params, the recovered .img won't mount.
export const STATE_VERSION = 5;
export const CONSTANTS = {
IMAGES_PATH,
IMAGE_PATH: path.join(IMAGES_PATH, "windows95.img"),
IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
DEFAULT_STATE_PATH: path.join(IMAGES_PATH, "default-state.bin"),
TOOLS_PATH: path.join(__dirname, "../../guest-tools"),
};
export const IPC_COMMANDS = {
@@ -17,6 +28,7 @@ export const IPC_COMMANDS = {
ZOOM_RESET: "ZOOM_RESET",
// Machine instructions
MACHINE_START: "MACHINE_START",
MACHINE_BOOT_FROM_SCRATCH: "MACHINE_BOOT_FROM_SCRATCH",
MACHINE_RESTART: "MACHINE_RESTART",
MACHINE_STOP: "MACHINE_STOP",
MACHINE_RESET: "MACHINE_RESET",
@@ -30,6 +42,8 @@ export const IPC_COMMANDS = {
// Else
APP_QUIT: "APP_QUIT",
GET_STATE_PATH: "GET_STATE_PATH",
GET_LEGACY_STATE_PATH: "GET_LEGACY_STATE_PATH",
GET_DOWNLOADS_PATH: "GET_DOWNLOADS_PATH",
GET_SMB_SHARE_PATH: "GET_SMB_SHARE_PATH",
SET_SMB_SHARE_PATH: "SET_SMB_SHARE_PATH",
PICK_FOLDER: "PICK_FOLDER",

View File

@@ -13,6 +13,13 @@
display: none;
margin: auto;
}
/* Guest absolute-mouse driver active: hide the host cursor over the VM so
only the Windows 95 cursor shows. Has to beat `* { cursor: default }`. */
&.seamless-mouse,
&.seamless-mouse * {
cursor: none;
}
}
.paused {

View File

@@ -1,15 +1,16 @@
@import "./status.less";
@import "./emulator.less";
@import "./info.less";
@import "./start.less";
@import "./settings.less";
@import "./status.css";
@import "./emulator.css";
@import "./info.css";
@import "./start.css";
@import "./settings.css";
// 98.css uses the actual MS Sans Serif bitmap font and pixel-exact bevels.
// Everything below is layout the chrome comes from 98.css.
/* 98.css uses the actual MS Sans Serif bitmap font and pixel-exact bevels.
Everything below is layout the chrome comes from 98.css. */
@win-teal: #008080;
@win-silver: silver;
@win-font: "Pixelated MS Sans Serif", Arial, sans-serif;
:root {
--win-teal: #008080;
--win-font: "Pixelated MS Sans Serif", Arial, sans-serif;
}
* {
user-select: none;
@@ -25,13 +26,13 @@ body {
body {
background: #000;
font-family: @win-font;
font-family: var(--win-font);
-webkit-font-smoothing: none;
image-rendering: pixelated;
}
body.paused {
background: @win-teal;
background: var(--win-teal);
> #emulator {
display: none;
@@ -48,8 +49,8 @@ button:focus {
outline: none;
}
// 98.css renders button text via text-shadow (color: transparent) so the
// bitmap font stays crisp; <img> children need their own alignment.
/* 98.css renders button text via text-shadow (color: transparent) so the
bitmap font stays crisp; <img> children need their own alignment. */
button img {
height: 16px;
width: 16px;
@@ -58,7 +59,7 @@ button img {
}
p {
font-family: @win-font;
font-family: var(--win-font);
font-size: 11px;
line-height: 1.5;
}

View File

@@ -41,7 +41,7 @@
input[type="text"] {
width: 100%;
font-family: @win-font;
font-family: var(--win-font);
}
input[type="text"]:read-only {

View File

@@ -1,4 +1,4 @@
// "Welcome to Windows" splash modelled on the real first-boot dialog.
/* "Welcome to Windows" splashmodelled on the real first-boot dialog. */
.welcome {
width: 540px;
@@ -80,6 +80,24 @@
}
}
.welcome-warn {
background: #fff;
p {
margin: 0 0 8px;
}
.welcome-warn-buttons {
display: flex;
gap: 6px;
margin-top: 4px;
button {
height: 24px;
}
}
}
.welcome-actions {
width: 130px;
display: flex;

View File

@@ -25,6 +25,24 @@
max-height: 18px;
top: 0;
transition: transform 0.12s ease-out;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
a.toggle {
display: inline-block;
width: 4ch;
text-align: center;
}
.spark {
vertical-align: -2px;
margin-right: 4px;
polyline {
fill: none;
stroke: currentColor;
stroke-width: 1;
}
}
&.hidden {
transform: translateX(-50%) translateY(-100%);

View File

@@ -1,15 +0,0 @@
export function encode(text: string) {
// Convert to windows-1252 compatible string by removing unsupported chars
let result = text.replaceAll(/[^\x00-\xFF]/g, "");
// If result would be empty, return original
if (!result.trim()) {
return text;
}
return result;
}
export function getEncoding() {
return `<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">`;
}

View File

@@ -1,25 +1,16 @@
import { protocol } from "electron";
import * as fs from "fs";
import * as path from "path";
import { generateDirectoryListing } from "./page-directory-listing";
import { generateErrorPage } from "./page-error";
import { log } from "../logging";
export interface FileEntry {
name: string;
fullPath: string;
stats: fs.Stats;
}
export const APP_INTERCEPT = "http://windows95/";
export const MY_COMPUTER_INTERCEPT = "http://my-computer/";
const interceptedUrls = [MY_COMPUTER_INTERCEPT, APP_INTERCEPT];
// Serves the bundled static/www site to the guest at http://windows95/.
// Host-filesystem browsing was removed in favour of the SMB share.
const APP_INTERCEPT = "http://windows95/";
const WWW_ROOT = path.resolve(__dirname, "../../../static/www");
export function setupFileServer() {
// Register protocol handler for our custom schema
protocol.handle("http", async (request) => {
if (!interceptedUrls.some((url) => request.url.startsWith(url))) {
if (!request.url.startsWith(APP_INTERCEPT)) {
return fetch(request.url, {
headers: request.headers,
method: request.method,
@@ -28,137 +19,46 @@ export function setupFileServer() {
}
try {
const { fullPath, decodedPath } = getFilePath(request.url);
log(`FileServer: Handling request for ${request.url}`, {
fullPath,
decodedPath,
});
// Check if path exists
if (!fs.existsSync(fullPath)) {
return new Response(
generateErrorPage("File or Directory Not Found", decodedPath),
{
status: 404,
headers: {
"Content-Type": "text/html",
},
},
);
const rel = decodeURIComponent(request.url.slice(APP_INTERCEPT.length));
let fullPath = path.join(WWW_ROOT, rel);
if (fullPath !== WWW_ROOT && !fullPath.startsWith(WWW_ROOT + path.sep)) {
fullPath = WWW_ROOT;
}
log(`FileServer: ${request.url}${fullPath}`);
// Check if it's a directory
const stats = await fs.promises.stat(fullPath);
if (stats.isDirectory()) {
// If we're in an app-intercept, check if there's an index.htm file in the directory
if (request.url.startsWith(APP_INTERCEPT)) {
const indexHtmlPath = path.join(fullPath, "index.htm");
if (fs.existsSync(indexHtmlPath)) {
return serveFile(indexHtmlPath);
}
}
// Generate directory listing
const files = await fs.promises.readdir(fullPath);
const listing = generateDirectoryListing(fullPath, files);
return new Response(listing, {
status: 200,
headers: {
"Content-Type": "text/html",
},
});
} else {
try {
return await serveFile(fullPath);
} catch (error) {
// Handle specific file read errors
if ((error as NodeJS.ErrnoException).code === "EACCES") {
return new Response(
generateErrorPage(
"Access Denied",
"You do not have permission to access this file",
),
{
status: 403,
headers: {
"Content-Type": "text/html",
},
},
);
}
// Re-throw other errors to be caught by outer try-catch
throw error;
}
}
if (stats.isDirectory()) fullPath = path.join(fullPath, "index.htm");
return await serveFile(fullPath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const errorPage = generateErrorPage(
"Internal Server Error",
`An error occurred while processing your request: ${message}`,
);
return new Response(errorPage, {
status: 500,
headers: {
"Content-Type": "text/html",
},
const code = (error as NodeJS.ErrnoException).code;
const status = code === "ENOENT" ? 404 : code === "EACCES" ? 403 : 500;
return new Response(`${status} ${code ?? "Error"}: ${request.url}`, {
status,
headers: { "Content-Type": "text/plain" },
});
}
});
}
function getFilePath(url: string) {
let urlPath: string;
let fullPath: string;
let decodedPath: string;
if (url.startsWith(APP_INTERCEPT)) {
fullPath = path.resolve(
__dirname,
"../../../static/www",
url.replace(APP_INTERCEPT, ""),
);
decodedPath = ".";
} else if (url.startsWith(MY_COMPUTER_INTERCEPT)) {
urlPath = url.replace(MY_COMPUTER_INTERCEPT, "");
decodedPath = decodeURIComponent(urlPath);
fullPath = path.join("/", decodedPath);
} else {
throw new Error("Invalid URL");
}
return { fullPath, decodedPath };
}
const CONTENT_TYPES: Record<string, string> = {
".htm": "text/html",
".html": "text/html",
".txt": "text/plain",
".css": "text/css",
".js": "text/javascript",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
};
async function serveFile(fullPath: string): Promise<Response> {
const fileData = await fs.promises.readFile(fullPath);
// Determine content type based on file extension
const ext = path.extname(fullPath).toLowerCase();
let contentType = "application/octet-stream";
// Common content types
const contentTypes: Record<string, string> = {
".htm": "text/html",
".html": "text/html",
".txt": "text/plain",
".css": "text/css",
".js": "text/javascript",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
};
if (ext in contentTypes) {
contentType = contentTypes[ext];
}
return new Response(fileData, {
status: 200,
headers: {
"Content-Type": contentType,
"Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream",
},
});
}

View File

@@ -1,71 +0,0 @@
import { settings } from "../settings";
import { FileEntry } from "./fileserver";
const FILES_TO_HIDE_ON_DARWIN: string[] = [
".DS_Store",
".localized",
".Trashes",
".fseventsd",
".Spotlight-V100",
".file",
".hotfiles.btree",
".DocumentRevisions-V100",
".TemporaryItems",
".file (resource fork files)",
".VolumeIcon.icns",
];
const FILES_TO_HIDE_ON_WINDOWS: string[] = [
"desktop.ini",
"Thumbs.db",
"ehthumbs.db",
"ehthumbs.db-shm",
"ehthumbs.db-wal",
];
const FILES_TO_HIDE_ON_LINUX: string[] = [];
export function shouldHideFile(file: FileEntry) {
if (isHiddenFile(file) && !settings.get("isFileServerShowingHiddenFiles")) {
return true;
}
if (
isSystemHiddenFile(file) &&
!settings.get("isFileServerShowingSystemHiddenFiles")
) {
return true;
}
return false;
}
export function isHiddenFile(file: FileEntry) {
if (process.platform === "win32") {
return (file.stats.mode & 0x2) === 0x2;
} else {
return file.name.startsWith(".");
}
}
export function isSystemHiddenFile(file: FileEntry) {
return getFilesToHide().some((hiddenFile) => file.name.endsWith(hiddenFile));
}
let _filesToHide: string[];
function getFilesToHide() {
if (_filesToHide) {
return _filesToHide;
}
if (process.platform === "darwin") {
_filesToHide = FILES_TO_HIDE_ON_DARWIN;
} else if (process.platform === "win32") {
_filesToHide = FILES_TO_HIDE_ON_WINDOWS;
} else {
_filesToHide = FILES_TO_HIDE_ON_LINUX;
}
return _filesToHide;
}

View File

@@ -1,126 +0,0 @@
import path from "path";
import fs from "fs";
import { APP_INTERCEPT, FileEntry, MY_COMPUTER_INTERCEPT } from "./fileserver";
import { shouldHideFile } from "./hide-files";
import { encode, getEncoding } from "./encoding";
import { log } from "console";
import { app } from "electron";
export function generateDirectoryListing(
currentPath: string,
files: string[],
): string {
const parentPath = path.dirname(currentPath || "/");
const title =
currentPath === "/"
? "My Host Computer"
: `Directory: ${encode(currentPath)}`;
// Get file info and sort (directories first, then alphabetically)
const items = files
.map((name) => {
const fullPath = path.join(currentPath, name);
try {
const stats = fs.statSync(fullPath);
return { name, fullPath, stats } as FileEntry;
} catch (error) {
log(`FileServer: Failed to get stats for ${fullPath}: ${error}`);
return null;
}
})
.filter(
(entry): entry is FileEntry => entry !== null && !shouldHideFile(entry),
)
.sort((a, b) => {
if (a.stats.isDirectory() !== b.stats.isDirectory()) {
return a.stats.isDirectory() ? -1 : 1;
}
return a.name.localeCompare(b.name);
})
.map(getFileLiHtml)
.join("");
// Generate very simple HTML that works in IE 5.5
return `
<html>
<head>
${getEncoding()}
<title>${title}</title>
</head>
<body>
<h2>${title}</h2>
<p>${getParentFolderLinkHtml(parentPath)} | ${getDesktopLinkHtml()} | ${getDownloadsLinkHtml()}</p>
<p>
<ul>
${items}
</ul>
</body>
</html>
`;
}
function getParentFolderLinkHtml(parentPath: string) {
return `
${getIconHtml("folder.gif")}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(parentPath)}">
[Parent Directory]
</a>
`;
}
function getDesktopLinkHtml() {
const desktopPath = app.getPath("desktop");
return `
${getIconHtml("desktop.gif")}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(desktopPath)}">
Desktop
</a>
`;
}
function getDownloadsLinkHtml() {
const downloadsPath = app.getPath("downloads");
return `
${getIconHtml("network.gif")}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(downloadsPath)}">
Downloads
</a>
`;
}
function getIconHtml(icon: string) {
return `<img src="${APP_INTERCEPT}images/${icon}" style="vertical-align: middle; margin-right: 5px;" width="16" height="16">`;
}
function getFileLiHtml(entry: FileEntry) {
const encodedPath = encodeURI(entry.fullPath);
const sizeDisplay = entry.stats.isDirectory()
? ""
: ` (${formatFileSize(entry.stats.size)})`;
const icon = entry.stats.isDirectory()
? getIconHtml("folder.gif")
: getIconHtml("doc.gif");
return `<li>
${icon}
<a href="${MY_COMPUTER_INTERCEPT}${encodedPath}">
${getDisplayName(entry)}
</a>
${sizeDisplay}
</li>`;
}
function getDisplayName(entry: FileEntry) {
return encode(entry.stats.isDirectory() ? `[${entry.name}]` : entry.name);
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}

View File

@@ -1,25 +0,0 @@
import { getEncoding } from "./encoding";
import { MY_COMPUTER_INTERCEPT } from "./fileserver";
export function generateErrorPage(
errorMessage: string,
requestedPath: string,
): string {
return `
<html>
<head>
${getEncoding()}
<title>Error - File Not Found</title>
</head>
<body>
<h2>Error: ${errorMessage}</h2>
<p>windows95 failed to find the file or directory on your host computer: <code>${requestedPath}</code></p>
<p>Options:</p>
<ul>
<li><a href="${MY_COMPUTER_INTERCEPT}">Return to root directory</a></li>
<li><a href="javascript:history.back()">Go back to previous page</a></li>
</ul>
</body>
</html>
`;
}

View File

@@ -2,12 +2,31 @@ import { ipcMain, app, dialog, BrowserWindow } from "electron";
import * as path from "path";
import * as fs from "fs";
import { IPC_COMMANDS } from "../constants";
import { IPC_COMMANDS, STATE_VERSION } from "../constants";
import { settings } from "./settings";
const statePathFor = (v: number) =>
path.join(app.getPath("userData"), `state-v${v}.bin`);
export function setupIpcListeners() {
ipcMain.handle(IPC_COMMANDS.GET_STATE_PATH, () => {
return path.join(app.getPath("userData"), "state-v4.bin");
return statePathFor(STATE_VERSION);
});
ipcMain.handle(IPC_COMMANDS.GET_LEGACY_STATE_PATH, () => {
// If the user already has a current-version state, there's nothing to
// rescue — either they've migrated or never had an older one.
if (fs.existsSync(statePathFor(STATE_VERSION))) return null;
// v2/v3 predate the overlay-rescue machinery and aren't worth supporting.
for (let v = STATE_VERSION - 1; v >= 4; v--) {
const p = statePathFor(v);
if (fs.existsSync(p)) return p;
}
return null;
});
ipcMain.handle(IPC_COMMANDS.GET_DOWNLOADS_PATH, () => {
return app.getPath("downloads");
});
ipcMain.handle(IPC_COMMANDS.APP_QUIT, () => {

View File

@@ -64,7 +64,10 @@ export function main() {
if (isDevMode()) {
// Renderer DevTools Protocol — connect Chrome to chrome://inspect
// or attach a debugger to localhost:9222
app.commandLine.appendSwitch("remote-debugging-port", "9222");
app.commandLine.appendSwitch(
"remote-debugging-port",
process.env.WIN95_DEBUG_PORT || "9222",
);
}
// Set the app's name

View File

@@ -125,29 +125,6 @@ async function createMenu({ isRunning } = { isRunning: false }) {
{
label: "Machine",
submenu: [
{
label: "Send Ctrl+Alt+Del",
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL),
enabled: isRunning,
},
{
label: "Send Alt+F4",
click: () => send(IPC_COMMANDS.MACHINE_ALT_F4),
enabled: isRunning,
},
{
label: "Send Alt+Enter",
click: () => send(IPC_COMMANDS.MACHINE_ALT_ENTER),
enabled: isRunning,
},
{
label: "Send Esc",
click: () => send(IPC_COMMANDS.MACHINE_ESC),
enabled: isRunning,
},
{
type: "separator",
},
isRunning
? {
label: "Stop",
@@ -157,6 +134,10 @@ async function createMenu({ isRunning } = { isRunning: false }) {
label: "Start",
click: () => send(IPC_COMMANDS.MACHINE_START),
},
{
label: "Start without state",
click: () => send(IPC_COMMANDS.MACHINE_BOOT_FROM_SCRATCH),
},
{
label: "Restart",
click: () => send(IPC_COMMANDS.MACHINE_RESTART),
@@ -184,6 +165,29 @@ async function createMenu({ isRunning } = { isRunning: false }) {
{
type: "separator",
},
{
label: "Send Ctrl+Alt+Del",
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL),
enabled: isRunning,
},
{
label: "Send Alt+F4",
click: () => send(IPC_COMMANDS.MACHINE_ALT_F4),
enabled: isRunning,
},
{
label: "Send Alt+Enter",
click: () => send(IPC_COMMANDS.MACHINE_ALT_ENTER),
enabled: isRunning,
},
{
label: "Send Esc",
click: () => send(IPC_COMMANDS.MACHINE_ESC),
enabled: isRunning,
},
{
type: "separator",
},
{
label: "Go to Disk Image",
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE),

View File

@@ -3,16 +3,10 @@ import * as path from "path";
import { app } from "electron";
export interface Settings {
isFileServerEnabled: boolean;
isFileServerShowingHiddenFiles: boolean;
isFileServerShowingSystemHiddenFiles: boolean;
smbSharePath: string;
}
const DEFAULT_SETTINGS: Settings = {
isFileServerEnabled: true,
isFileServerShowingHiddenFiles: false,
isFileServerShowingSystemHiddenFiles: false,
smbSharePath: app.getPath("downloads"),
};
@@ -60,11 +54,6 @@ class SettingsManager {
this.save();
}
delete(key: keyof Settings): void {
delete this.data[key];
this.save();
}
clear(): void {
this.data = DEFAULT_SETTINGS;
this.save();

View File

@@ -1,7 +1,28 @@
import { BrowserWindow, shell } from "electron";
import { execFileSync } from "child_process";
import { isDevMode } from "../utils/devmode";
let mainWindow: BrowserWindow | null = null;
function getDevBranchSuffix(): string {
if (!isDevMode()) return "";
try {
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
encoding: "utf8",
}).trim();
if (branch && branch !== "master" && branch !== "main") {
return ` (${branch})`;
}
} catch {
// git not available or not a repo — ignore
}
return "";
}
export function getOrCreateWindow(): BrowserWindow {
if (mainWindow) return mainWindow;
@@ -18,6 +39,14 @@ export function getOrCreateWindow(): BrowserWindow {
},
});
const branchSuffix = getDevBranchSuffix();
if (branchSuffix) {
mainWindow.on("page-title-updated", (event, title) => {
event.preventDefault();
mainWindow?.setTitle(`${title}${branchSuffix}`);
});
}
mainWindow.loadFile("./dist/static/index.html");
mainWindow.webContents.on("will-navigate", (event, url) =>

View File

@@ -1,3 +1,6 @@
import "../css/vendor/98.css";
import "../css/root.css";
export interface Win95Window extends Window {
emulator: any;
win95: {

View File

@@ -1,9 +1,9 @@
import * as React from "react";
import { resetState } from "./utils/reset-state";
import { InfoBarSettings } from "./info-bar-settings";
// v86's IDE CD-ROM path is currently broken; flip this once it works again.
const CDROM_ENABLED = false;
const CDROM_ENABLED = true;
interface CardSettingsProps {
bootFromScratch: () => void;
@@ -15,9 +15,11 @@ interface CardSettingsProps {
floppy?: File;
cdrom?: File;
smbSharePath: string;
infoBarSettings: InfoBarSettings;
setInfoBarSettings: (s: InfoBarSettings) => void;
}
type Tab = "floppy" | "cdrom" | "network" | "state";
type Tab = "floppy" | "cdrom" | "network" | "interface" | "state";
interface CardSettingsState {
tab: Tab;
@@ -60,7 +62,8 @@ export class CardSettings extends React.Component<
<menu role="tablist">
{this.renderTab("floppy", "Floppy Drive")}
{CDROM_ENABLED && this.renderTab("cdrom", "CD-ROM")}
{this.renderTab("network", "Network Share")}
{this.renderTab("network", "Shared Folder")}
{this.renderTab("interface", "Interface")}
{this.renderTab("state", "Machine State")}
</menu>
<div className="window settings-panel" role="tabpanel">
@@ -68,6 +71,7 @@ export class CardSettings extends React.Component<
{tab === "floppy" && this.renderFloppy()}
{tab === "cdrom" && this.renderCdrom()}
{tab === "network" && this.renderSmbShare()}
{tab === "interface" && this.renderInterface()}
{tab === "state" && this.renderState()}
</div>
</div>
@@ -186,14 +190,15 @@ export class CardSettings extends React.Component<
return (
<fieldset>
<legend>\\HOST\HOST</legend>
<legend>Drive Z:</legend>
<div className="settings-row">
<img className="settings-icon" src="../../static/show-disk-image.png" />
<img
className="settings-icon"
src="../../static/show-disk-image.png"
/>
<p>
A folder on your computer is exposed inside Windows 95 as a network
drive. From inside Windows, open Start → Run and type{" "}
<code>\\HOST\HOST</code> — or use Map Network Drive to give it a
letter.
A folder on your computer is mounted inside Windows 95 as drive{" "}
<code>Z:</code>. Open My Computer inside Windows to find it.
</p>
</div>
<div className="field-row-stacked">
@@ -214,6 +219,42 @@ export class CardSettings extends React.Component<
);
}
private renderInterface() {
const { infoBarSettings, setInfoBarSettings } = this.props;
const checkbox = (key: keyof InfoBarSettings, label: string) => (
<div className="field-row">
<input
id={`ibs-${key}`}
type="checkbox"
checked={infoBarSettings[key]}
onChange={(e) =>
setInfoBarSettings({ ...infoBarSettings, [key]: e.target.checked })
}
/>
<label htmlFor={`ibs-${key}`}>{label}</label>
</div>
);
return (
<fieldset>
<legend>Info bar</legend>
<div className="settings-row">
<img className="settings-icon" src="../../static/settings.png" />
<p>
The bar at the top of the emulator shows live machine stats. Choose
which metrics to display and whether to draw sparkline graphs next
to them.
</p>
</div>
{checkbox("showCpu", "Show CPU speed")}
{checkbox("showDisk", "Show disk throughput")}
{checkbox("showNet", "Show network throughput")}
{checkbox("showSparklines", "Show sparklines")}
</fieldset>
);
}
private renderState() {
const { isStateReset } = this.state;
const { bootFromScratch } = this.props;

View File

@@ -3,12 +3,19 @@ import * as React from "react";
export interface CardStartProps {
startEmulator: () => void;
navigate: (to: "start" | "settings") => void;
legacyStatePath: string | null;
legacyRecovered: { dir: string; files: number } | null;
legacyRecoverBusy: boolean;
legacyRecoverError: string | null;
recoverLegacy: () => void;
showRecovered: () => void;
discardLegacy: () => void;
}
const TIPS = [
"Press the Escape key at any time to release or recapture your mouse cursor.",
"You can mount a floppy image from Settings before booting to install vintage software.",
"Map a host folder as a network drive: open Start → Run inside Windows and type \\\\HOST\\HOST.",
"A folder from your real computer is mounted as drive Z: — open My Computer inside Windows to find it.",
"Your machine state is saved automatically when you quit. Reset it from Settings if things get weird.",
"Use the Machine menu in the menubar to send Ctrl+Alt+Del and other special key combos.",
];
@@ -37,12 +44,9 @@ export class CardStart extends React.Component<CardStartProps> {
<small>95</small>
</h1>
<div className="welcome-tip">
<div className="welcome-tip-header">
<strong>Did you know...</strong>
</div>
<p>{this.tip}</p>
</div>
{this.props.legacyStatePath
? this.renderLegacyNotice()
: this.renderTip()}
</div>
<div className="welcome-actions">
<button
@@ -62,4 +66,102 @@ export class CardStart extends React.Component<CardStartProps> {
</div>
);
}
private renderTip() {
return (
<div className="welcome-tip">
<div className="welcome-tip-header">
<strong>Did you know...</strong>
</div>
<p>{this.tip}</p>
</div>
);
}
private renderLegacyNotice() {
const { legacyRecovered, legacyRecoverBusy, legacyRecoverError } =
this.props;
if (legacyRecoverError) {
return (
<div className="welcome-tip welcome-warn">
<div className="welcome-tip-header">
<strong>Recovery failed</strong>
</div>
<p>
The old snapshot's format isn't compatible with the bundled
emulator, so files couldn't be extracted automatically. The snapshot
has been kept on disk.
</p>
<p>
<code>{legacyRecoverError}</code>
</p>
<div className="welcome-warn-buttons">
<button onClick={this.props.discardLegacy}>
Discard old snapshot
</button>
</div>
</div>
);
}
if (legacyRecovered) {
return (
<div className="welcome-tip welcome-warn">
<div className="welcome-tip-header">
<strong>Old C:\ recovered</strong>
</div>
<p>
{legacyRecovered.files} file
{legacyRecovered.files === 1 ? "" : "s"} you created or modified
have been copied out as ordinary files. Starting Windows here will
be a fresh machine.
</p>
<p>
<code>{legacyRecovered.dir}</code>
</p>
<div className="welcome-warn-buttons">
<button className="default" onClick={this.props.showRecovered}>
Open folder
</button>
<button onClick={this.props.discardLegacy}>
Discard old snapshot
</button>
</div>
</div>
);
}
return (
<div className="welcome-tip welcome-warn">
<div className="welcome-tip-header">
<strong>Your saved machine is from an older version</strong>
</div>
<p>
This release ships a new disk image and machine configuration. Files
you saved to <code>C:\</code> live only in the old snapshot.
</p>
<p>
Recovery copies anything you created or modified out to an ordinary
folder on this computer no booting, no disk images. Pre-installed
programs are skipped.
</p>
<div className="welcome-warn-buttons">
<button
className="default"
disabled={legacyRecoverBusy}
onClick={this.props.recoverLegacy}
>
{legacyRecoverBusy ? "Recovering…" : "Recover old C:\\ drive…"}
</button>
<button
disabled={legacyRecoverBusy}
onClick={this.props.discardLegacy}
>
Discard it
</button>
</div>
</div>
);
}
}

62
src/renderer/clipboard.ts Normal file
View File

@@ -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 69) 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);
};
}

View File

@@ -4,8 +4,8 @@
import * as fs from "fs";
const STATUS_FILE = "/tmp/win95-probe.json";
const SCREEN_FILE = "/tmp/win95-screen.png";
const STATUS_FILE = process.env.WIN95_PROBE_STATUS || "/tmp/win95-probe.json";
const SCREEN_FILE = process.env.WIN95_PROBE_SCREEN || "/tmp/win95-screen.png";
const TICK_MS = 5000;
interface ProbeStatus {
@@ -20,7 +20,16 @@ interface ProbeStatus {
gfxW: number;
gfxH: number;
dominantColor: string;
verdict: "" | "SUCCESS" | "FAIL_IOS" | "FAIL_KRNL386" | "FAIL_VXDLINK" | "FAIL_PROTECTION" | "FAIL_SPLASH_HANG" | "FAIL_HUNG" | "FAIL_OTHER";
verdict:
| ""
| "SUCCESS"
| "FAIL_IOS"
| "FAIL_KRNL386"
| "FAIL_VXDLINK"
| "FAIL_PROTECTION"
| "FAIL_SPLASH_HANG"
| "FAIL_HUNG"
| "FAIL_OTHER";
}
let startTime = 0;
@@ -31,17 +40,146 @@ let stableTextTicks = 0;
// XT scancodes (set 1). Win95 doesn't have Win+R — that landed in Win98.
// Ctrl+Esc opens Start, then R is the underlined mnemonic for "Run...".
const SC = {
CTRL_DN: [0x1d], CTRL_UP: [0x9d],
ESC_DN: [0x01], ESC_UP: [0x81],
R_DN: [0x13], R_UP: [0x93],
ENTER_DN: [0x1c], ENTER_UP: [0x9c],
BACKSLASH_DN: [0x2b], BACKSLASH_UP: [0xab],
CTRL_DN: [0x1d],
CTRL_UP: [0x9d],
ESC_DN: [0x01],
ESC_UP: [0x81],
R_DN: [0x13],
R_UP: [0x93],
ENTER_DN: [0x1c],
ENTER_UP: [0x9c],
BACKSLASH_DN: [0x2b],
BACKSLASH_UP: [0xab],
ALT_DN: [0x38],
ALT_UP: [0xb8],
};
// WIN95_PROBE_CDTRACE=1 → wrap secondary-IDE ata_command/atapi_handle and
// log every command so we can see whether Win95's ESDI_506/CDVSD stack ever
// talks to the drive (and which ATAPI CDBs it sends).
const CDTRACE_FILE =
process.env.WIN95_PROBE_CDTRACE_FILE || "/tmp/win95-cdtrace.log";
let cdTraceArmed = false;
function armCdTrace(emulator: any) {
const dev = emulator.v86?.cpu?.devices;
if (!dev || cdTraceArmed) return;
cdTraceArmed = true;
const sec = dev.ide?.secondary;
fs.writeFileSync(
CDTRACE_FILE,
`[probe] cd buffer=${!!dev.cdrom?.buffer} bytes=${dev.cdrom?.buffer?.byteLength} is_atapi=${sec?.master?.is_atapi}\n`,
);
const t0 = Date.now();
const log = (s: string) =>
fs.appendFileSync(
CDTRACE_FILE,
`[${((Date.now() - t0) / 1000).toFixed(2)}s] ${s}\n`,
);
const proto = Object.getPrototypeOf(sec?.master || {});
for (const m of ["ata_command", "atapi_handle"]) {
const orig = proto?.[m];
if (typeof orig !== "function") continue;
proto[m] = function (this: any, ...a: any[]) {
if (this === sec?.master || this === sec?.slave) {
const who = this === sec.master ? "sm" : "ss";
if (m === "ata_command")
log(`${who} ata cmd=0x${(a[0] ?? 0).toString(16)}`);
else {
const d = this.data || [];
const cdb = Array.from(d.slice?.(0, 12) || [])
.map((b: any) => b.toString(16).padStart(2, "0"))
.join(" ");
log(`${who} atapi cmd=0x${(d[0] ?? 0).toString(16)} cdb=[${cdb}]`);
}
}
return orig.apply(this, a);
};
}
console.log("[probe] cd trace armed");
}
// WIN95_PROBE_VGATRACE=1 → wrap VGA I/O ports at the io.ports[] layer (the
// VGAScreen.portXXX_write methods are captured by-value at registration time,
// so monkey-patching them on the instance is a no-op for most ports). Each
// entry is [port, op, value, "eip VMPE cplN"] so you can tell vgabios in V86
// mode apart from the ring-0 display driver.
const VGATRACE_FILE = "/tmp/win95-vgatrace.json";
let vgaTrace: any[] | undefined;
function armVgaTrace(emulator: any) {
const cpu = emulator.v86?.cpu;
const io = cpu?.io;
if (!io || vgaTrace) return;
vgaTrace = [];
const ctx = () => {
try {
const ip = (cpu.instruction_pointer[0] >>> 0).toString(16);
const vm = cpu.flags[0] & (1 << 17) ? "VM" : " ";
const pe = cpu.cr[0] & 1 ? "PE" : " ";
return `${ip} ${vm}${pe} cpl${cpu.cpl[0]}`;
} catch {
return "?";
}
};
const W = [
0x3c0, 0x3c2, 0x3c4, 0x3c5, 0x3ce, 0x3cf, 0x3d4, 0x3d5, 0x3b4, 0x3b5, 0x1ce,
0x1cf,
];
const R = [0x1cf, 0x3da, 0x3c1];
for (const p of W)
for (const w of ["write8", "write16"]) {
const orig = io.ports[p][w];
io.ports[p][w] = function (v: number) {
vgaTrace!.push([p, w, v, ctx()]);
return orig.call(this, v);
};
}
for (const p of R)
for (const r of ["read8", "read16"]) {
const orig = io.ports[p][r];
io.ports[p][r] = function () {
const v = orig.call(this);
vgaTrace!.push([p, r, v, ctx()]);
return v;
};
}
console.log("[probe] vga trace armed");
}
function dumpVgaTrace(emulator: any) {
if (!vgaTrace) return;
const d = emulator.v86?.cpu?.devices?.vga;
const state = d && {
svga_enabled: d.svga_enabled,
graphical_mode: d.graphical_mode,
attribute_mode: d.attribute_mode,
miscellaneous_graphics_register: d.miscellaneous_graphics_register,
sequencer_memory_mode: d.sequencer_memory_mode,
clocking_mode: d.clocking_mode,
plane_write_bm: d.plane_write_bm,
crtc_mode: d.crtc_mode,
max_scan_line: d.max_scan_line,
underline_location_register: d.underline_location_register,
horizontal_display_enable_end: d.horizontal_display_enable_end,
horizontal_blank_start: d.horizontal_blank_start,
vertical_display_enable_end: d.vertical_display_enable_end,
vertical_blank_start: d.vertical_blank_start,
offset_register: d.offset_register,
dispi_enable_value: d.dispi_enable_value,
screen_width: d.screen_width,
screen_height: d.screen_height,
max_cols: d.max_cols,
max_rows: d.max_rows,
};
fs.writeFileSync(VGATRACE_FILE, JSON.stringify({ state, trace: vgaTrace }));
}
function sendChord(emu: any, ...keys: { dn: number[]; up: number[] }[]) {
for (const k of keys) emu.keyboard_send_scancodes(k.dn);
setTimeout(() => {
for (let i = keys.length - 1; i >= 0; i--) emu.keyboard_send_scancodes(keys[i].up);
for (let i = keys.length - 1; i >= 0; i--)
emu.keyboard_send_scancodes(keys[i].up);
}, 60);
}
@@ -54,11 +192,25 @@ function sendKey(emu: any, dn: number[], up: number[]) {
function runScript(emu: any, steps: any[]) {
let i = 0;
const next = () => {
if (i >= steps.length) { console.log("[probe] script done"); return; }
if (i >= steps.length) {
console.log("[probe] script done");
return;
}
const s = steps[i++];
if (s.type === "wait") { setTimeout(next, s.ms); return; }
if (s.type === "keys") { sendKey(emu, s.dn, s.up); setTimeout(next, 200); return; }
if (s.type === "chord") { sendChord(emu, ...s.keys); setTimeout(next, 200); return; }
if (s.type === "wait") {
setTimeout(next, s.ms);
return;
}
if (s.type === "keys") {
sendKey(emu, s.dn, s.up);
setTimeout(next, 200);
return;
}
if (s.type === "chord") {
sendChord(emu, ...s.keys);
setTimeout(next, 200);
return;
}
if (s.type === "text") {
// keyboard_send_text handles ASCII → scancode for us
emu.keyboard_send_text(s.text);
@@ -76,16 +228,36 @@ export function startProbe(emulator: any) {
// WIN95_PROBE_SCRIPT=\\HOST → after desktop, send Win+R, type, Enter
const scriptCmd = process.env.WIN95_PROBE_SCRIPT;
let scriptArmed = !!scriptCmd;
// WIN95_PROBE_RUN='telnet 1.2.3.4 7777' → literal text into Start→Run,
// Enter, then optional WIN95_PROBE_RUN_AFTER keystrokes after _RUN_WAIT ms.
// WIN95_PROBE_RUN2 fires a second Start→Run sequence after _RUN2_WAIT ms,
// for two-process scenarios (e.g., background ping + telnet).
const runCmd = process.env.WIN95_PROBE_RUN;
const runCmd2 = process.env.WIN95_PROBE_RUN2;
const runAfter = process.env.WIN95_PROBE_RUN_AFTER;
// WIN95_PROBE_DOSBOX=1 → after desktop, open COMMAND.COM, type `dir`,
// optionally Alt+Enter to fullscreen. Regression test for the windowed
// DOS box clobbering VBE (felixrieseberg/v86 vga-defer-vbe-disable-v86).
const dosBox = process.env.WIN95_PROBE_DOSBOX === "1";
const wantVgaTrace = process.env.WIN95_PROBE_VGATRACE === "1";
const wantCdTrace = process.env.WIN95_PROBE_CDTRACE === "1";
let scriptArmed = !!scriptCmd || !!runCmd || dosBox;
const tick = () => {
try {
if (wantVgaTrace && !vgaTrace) armVgaTrace(emulator);
if (wantCdTrace && !cdTraceArmed) armCdTrace(emulator);
const s = collectStatus(emulator);
fs.writeFileSync(STATUS_FILE, JSON.stringify(s, null, 2));
// Try to capture a screenshot — this can fail if the screen adapter
// isn't ready yet, so we swallow that.
try {
// rAF doesn't fire when the Electron window is occluded, so the
// screen adapter's render loop stalls. Pump one frame by hand.
try {
emulator.screen_adapter?.update_screen?.();
} catch {}
const img: HTMLImageElement = emulator.screen_make_screenshot();
// The Image has a data: URL src; decode it to bytes
if (img && img.src && img.src.startsWith("data:image/png;base64,")) {
@@ -94,24 +266,126 @@ export function startProbe(emulator: any) {
}
} catch {}
dumpVgaTrace(emulator);
// Once at desktop, fire the keyboard script (once). The 8s settle is
// for the "Welcome to Windows 95" tip dialog to be dismissable —
// we send Esc first to clear it.
if (scriptArmed && s.phase === "desktop" && s.uptimeSec > 8) {
scriptArmed = false;
if (dosBox) {
console.log("[probe] desktop detected, opening DOS box");
runScript(emulator, [
{ type: "wait", ms: 3000 },
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP },
{ type: "wait", ms: 1000 },
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP },
{ type: "wait", ms: 1000 },
{
type: "chord",
keys: [
{ dn: SC.CTRL_DN, up: SC.CTRL_UP },
{ dn: SC.ESC_DN, up: SC.ESC_UP },
],
},
{ type: "wait", ms: 1200 },
{ type: "keys", dn: SC.R_DN, up: SC.R_UP },
{ type: "wait", ms: 1000 },
{ type: "text", text: "command" },
{ type: "wait", ms: 400 },
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
{ type: "wait", ms: 5000 },
{ type: "text", text: "dir" },
{ type: "wait", ms: 200 },
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
{ type: "wait", ms: 3000 },
...(process.env.WIN95_PROBE_DOSBOX_ALTENTER === "1"
? [
{
type: "chord",
keys: [
{ dn: SC.ALT_DN, up: SC.ALT_UP },
{ dn: SC.ENTER_DN, up: SC.ENTER_UP },
],
},
{ type: "wait", ms: 4000 },
]
: []),
]);
return;
}
if (runCmd) {
console.log("[probe] desktop detected, Run →", runCmd);
runScript(emulator, [
{ type: "wait", ms: 3000 },
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP },
{ type: "wait", ms: 1000 },
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP },
{ type: "wait", ms: 1000 },
{
type: "chord",
keys: [
{ dn: SC.CTRL_DN, up: SC.CTRL_UP },
{ dn: SC.ESC_DN, up: SC.ESC_UP },
],
},
{ type: "wait", ms: 1200 },
{ type: "keys", dn: SC.R_DN, up: SC.R_UP },
{ type: "wait", ms: 1000 },
{ type: "text", text: runCmd },
{ type: "wait", ms: 400 },
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
...(runCmd2
? [
{
type: "wait",
ms: Number(process.env.WIN95_PROBE_RUN2_WAIT) || 3000,
},
{
type: "chord",
keys: [
{ dn: SC.CTRL_DN, up: SC.CTRL_UP },
{ dn: SC.ESC_DN, up: SC.ESC_UP },
],
},
{ type: "wait", ms: 1200 },
{ type: "keys", dn: SC.R_DN, up: SC.R_UP },
{ type: "wait", ms: 1000 },
{ type: "text", text: runCmd2 },
{ type: "wait", ms: 400 },
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
]
: []),
...(runAfter
? [
{
type: "wait",
ms: Number(process.env.WIN95_PROBE_RUN_WAIT) || 6000,
},
{ type: "text", text: runAfter },
{ type: "wait", ms: 200 },
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
]
: []),
]);
return;
}
console.log("[probe] desktop detected, running script:", scriptCmd);
runScript(emulator, [
{ type: "wait", ms: 3000 },
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP }, // dismiss any dialog
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP }, // dismiss any dialog
{ type: "wait", ms: 1000 },
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP }, // again, for safety
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP }, // again, for safety
{ type: "wait", ms: 1000 },
{ type: "chord", keys: [
{ dn: SC.CTRL_DN, up: SC.CTRL_UP },
{ dn: SC.ESC_DN, up: SC.ESC_UP },
]}, // Ctrl+Esc → Start
{
type: "chord",
keys: [
{ dn: SC.CTRL_DN, up: SC.CTRL_UP },
{ dn: SC.ESC_DN, up: SC.ESC_UP },
],
}, // Ctrl+Esc → Start
{ type: "wait", ms: 1200 },
{ type: "keys", dn: SC.R_DN, up: SC.R_UP }, // Run mnemonic
{ type: "keys", dn: SC.R_DN, up: SC.R_UP }, // Run mnemonic
{ type: "wait", ms: 1000 },
// keyboard_send_text can't reliably do backslash, so we interleave:
// scancode for each \ segment, text for each name segment.
@@ -119,9 +393,11 @@ export function startProbe(emulator: any) {
// the segment separator in the env var to dodge shell escaping hell)
...scriptCmd!.split("/").flatMap((seg, i) => [
...(i === 0
? [{ type: "keys", dn: SC.BACKSLASH_DN, up: SC.BACKSLASH_UP },
{ type: "wait", ms: 60 },
{ type: "keys", dn: SC.BACKSLASH_DN, up: SC.BACKSLASH_UP }]
? [
{ type: "keys", dn: SC.BACKSLASH_DN, up: SC.BACKSLASH_UP },
{ type: "wait", ms: 60 },
{ type: "keys", dn: SC.BACKSLASH_DN, up: SC.BACKSLASH_UP },
]
: [{ type: "keys", dn: SC.BACKSLASH_DN, up: SC.BACKSLASH_UP }]),
{ type: "wait", ms: 60 },
{ type: "text", text: seg },
@@ -149,9 +425,14 @@ function collectStatus(emulator: any): ProbeStatus {
const uptimeSec = (Date.now() - startTime) / 1000;
// CPU activity — instruction counter is u32 in wasm, wraps every ~4B
let instr = 0, running = false;
try { instr = emulator.get_instruction_counter() || 0; } catch {}
try { running = emulator.is_running(); } catch {}
let instr = 0,
running = false;
try {
instr = emulator.get_instruction_counter() || 0;
} catch {}
try {
running = emulator.is_running();
} catch {}
const instrDelta = (instr - lastInstr) >>> 0;
lastInstr = instr;
@@ -162,7 +443,10 @@ function collectStatus(emulator: any): ProbeStatus {
const screen = emulator.screen_adapter || emulator.v86?.screen_adapter;
if (screen) {
const rows = screen.get_text_screen?.() || [];
textScreen = rows.map((r: string) => r.trimEnd()).join("\n").trim();
textScreen = rows
.map((r: string) => r.trimEnd())
.join("\n")
.trim();
}
} catch {}
@@ -171,7 +455,9 @@ function collectStatus(emulator: any): ProbeStatus {
// Old v86 builds (pre-2025) don't expose screen_width/screen_height — fall
// back to the rendered canvas dimensions so the bisect harness works across
// versions.
let inGraphics = false, gfxW = 0, gfxH = 0;
let inGraphics = false,
gfxW = 0,
gfxH = 0;
try {
const vga = emulator.v86?.cpu?.devices?.vga;
if (vga) {
@@ -182,14 +468,19 @@ function collectStatus(emulator: any): ProbeStatus {
} catch {}
if (gfxW === 0) {
try {
const canvas = document.querySelector("#emulator canvas") as HTMLCanvasElement | null;
const canvas = document.querySelector(
"#emulator canvas",
) as HTMLCanvasElement | null;
if (canvas && canvas.width > 0) {
gfxW = canvas.width;
gfxH = canvas.height;
// Canvas exists with content → assume graphics. Text mode uses a div.
const textDiv = document.querySelector("#emulator div") as HTMLElement | null;
inGraphics = canvas.style.display !== "none" &&
(!textDiv || textDiv.style.display === "none");
const textDiv = document.querySelector(
"#emulator div",
) as HTMLElement | null;
inGraphics =
canvas.style.display !== "none" &&
(!textDiv || textDiv.style.display === "none");
}
} catch {}
}
@@ -199,7 +490,9 @@ function collectStatus(emulator: any): ProbeStatus {
let dominantColor = "";
if (inGraphics) {
try {
const canvas = document.querySelector("#emulator canvas") as HTMLCanvasElement | null;
const canvas = document.querySelector(
"#emulator canvas",
) as HTMLCanvasElement | null;
if (canvas) {
const ctx = canvas.getContext("2d")!;
const cx = Math.floor(canvas.width / 2);
@@ -215,45 +508,66 @@ function collectStatus(emulator: any): ProbeStatus {
else stableTextTicks = 0;
lastTextHash = textHash;
const hasMeaningfulText = !inGraphics && textScreen.length > 20 && /[A-Za-z]{4,}/.test(textScreen);
const hasMeaningfulText =
!inGraphics && textScreen.length > 20 && /[A-Za-z]{4,}/.test(textScreen);
const atSplash = inGraphics && gfxW > 0 && gfxW < 640;
const atDesktop = inGraphics && gfxW >= 640;
const phase: ProbeStatus["phase"] =
!running ? "init" :
atDesktop ? "desktop" :
atSplash ? "splash" :
hasMeaningfulText ? "text-mode" :
"running";
const phase: ProbeStatus["phase"] = !running
? "init"
: atDesktop
? "desktop"
: atSplash
? "splash"
: hasMeaningfulText
? "text-mode"
: "running";
let verdict: ProbeStatus["verdict"] = "";
const t = inGraphics ? "" : textScreen.toLowerCase();
if (t.includes("krnl386")) verdict = "FAIL_KRNL386";
else if (t.includes("vxd dynamic link")) verdict = "FAIL_VXDLINK";
else if (t.includes("initializing device ios") && t.includes("protection error")) verdict = "FAIL_IOS";
else if (
t.includes("initializing device ios") &&
t.includes("protection error")
)
verdict = "FAIL_IOS";
else if (t.includes("windows protection error")) verdict = "FAIL_PROTECTION";
// Stuck at splash for >70s with CPU spinning → IDE IRQ never fired
else if (atSplash && uptimeSec > 70) verdict = "FAIL_SPLASH_HANG";
// Stuck on text for 40s
else if (stableTextTicks >= 8 && instrDelta > 1_000_000) verdict = "FAIL_HUNG";
else if (stableTextTicks >= 8 && instrDelta > 1_000_000)
verdict = "FAIL_HUNG";
// CPU dead
else if (running && instrDelta < 1000 && uptimeSec > 30) verdict = "FAIL_HUNG";
else if (running && instrDelta < 1000 && uptimeSec > 30)
verdict = "FAIL_HUNG";
// Made it to ≥640×480 graphics → desktop reached. But if a keyboard
// script is running, hold off — the outer harness reads the SMB log
// directly and we just keep the app alive.
else if (atDesktop && uptimeSec > 30 && !process.env.WIN95_PROBE_SCRIPT) verdict = "SUCCESS";
else if (
atDesktop &&
uptimeSec > 30 &&
!process.env.WIN95_PROBE_SCRIPT &&
!process.env.WIN95_PROBE_RUN &&
!process.env.WIN95_PROBE_DOSBOX
)
verdict = "SUCCESS";
// Timeout
else if (uptimeSec > 180) verdict = "FAIL_OTHER";
return {
ts: new Date().toISOString(),
uptimeSec: Math.round(uptimeSec),
phase, cpuRunning: running,
phase,
cpuRunning: running,
instructionCounter: instr,
instructionDelta: instrDelta,
textScreen: textScreen.slice(0, 2000),
textHash, gfxW, gfxH, dominantColor,
textHash,
gfxW,
gfxH,
dominantColor,
verdict,
};
}

View File

@@ -1,9 +1,11 @@
import * as React from "react";
import { InfoBarSettings } from "./info-bar-settings";
interface EmulatorInfoProps {
toggleInfo: () => void;
emulator: any;
hidden: boolean;
settings: InfoBarSettings;
}
interface EmulatorInfoState {
@@ -14,6 +16,30 @@ interface EmulatorInfoState {
netTx: number;
lastCounter: number;
lastTick: number;
history: {
cpu: number[];
diskRead: number[];
diskWrite: number[];
netRx: number[];
netTx: number[];
};
}
const HISTORY_LEN = 30;
function Sparkline({ data }: { data: number[] }) {
const w = 20;
const h = 12;
const max = Math.max(1, ...data);
const step = data.length > 1 ? w / (data.length - 1) : 0;
const points = data
.map((v, i) => `${i * step},${h - (v / max) * h}`)
.join(" ");
return (
<svg className="spark" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
<polyline points={points} />
</svg>
);
}
export class EmulatorInfo extends React.Component<
@@ -43,22 +69,48 @@ export class EmulatorInfo extends React.Component<
netTx: 0,
lastCounter: 0,
lastTick: 0,
history: {
cpu: new Array(HISTORY_LEN).fill(0),
diskRead: new Array(HISTORY_LEN).fill(0),
diskWrite: new Array(HISTORY_LEN).fill(0),
netRx: new Array(HISTORY_LEN).fill(0),
netTx: new Array(HISTORY_LEN).fill(0),
},
};
}
public render() {
const { cpu, diskRead, diskWrite, netRx, netTx } = this.state;
const { hidden, toggleInfo } = this.props;
const { cpu, diskRead, diskWrite, netRx, netTx, history } = this.state;
const { hidden, toggleInfo, settings } = this.props;
const { showCpu, showDisk, showNet, showSparklines: spark } = settings;
return (
<>
<div id="status-hotzone" />
<div id="status" className={hidden ? "hidden" : ""}>
CPU: <span>{cpu}M/s</span> | Disk:{" "}
<span>R {this.rate(diskRead)}</span>{" "}
<span>W {this.rate(diskWrite)}</span> | Net:{" "}
<span>{this.rate(netRx)}</span> <span>{this.rate(netTx)}</span> |{" "}
<a href="#" onClick={toggleInfo}>
{showCpu && (
<>
CPU: {spark && <Sparkline data={history.cpu} />}
{this.fmt(cpu, ["M", "G"])}/s |{" "}
</>
)}
{showDisk && (
<>
Disk: {spark && <Sparkline data={history.diskRead} />}R{" "}
{this.fmt(diskRead, ["B", "K", "M", "G"])}/s{" "}
{spark && <Sparkline data={history.diskWrite} />}W{" "}
{this.fmt(diskWrite, ["B", "K", "M", "G"])}/s |{" "}
</>
)}
{showNet && (
<>
Net: {spark && <Sparkline data={history.netRx} />}
{this.fmt(netRx, ["B", "K", "M", "G"])}/s{" "}
{spark && <Sparkline data={history.netTx} />}
{this.fmt(netTx, ["B", "K", "M", "G"])}/s |{" "}
</>
)}
<a href="#" className="toggle" onClick={toggleInfo}>
{hidden ? "Pin" : "Hide"}
</a>
</div>
@@ -152,13 +204,19 @@ export class EmulatorInfo extends React.Component<
}
/**
* Format bytes/sec into a compact human string.
* Format a value as "N.NU" by walking the unit ladder until it fits in
* one digit before the decimal. Always exactly 4 chars (e.g. "0.0B",
* "3.2K", "9.9G") so the bar width never changes.
*/
private rate(bytesPerSec: number) {
if (bytesPerSec <= 0) return "0";
if (bytesPerSec < 1024) return `${bytesPerSec}B/s`;
if (bytesPerSec < 1024 * 1024) return `${Math.round(bytesPerSec / 1024)}K/s`;
return `${(bytesPerSec / 1024 / 1024).toFixed(1)}M/s`;
private fmt(value: number, units: string[]) {
let v = Math.max(0, value);
let u = 0;
while (v >= 10 && u < units.length - 1) {
v /= 1000;
u++;
}
if (v >= 9.95) v = 9.9;
return `${v.toFixed(1)}${units[u]}`;
}
/**
@@ -173,15 +231,30 @@ export class EmulatorInfo extends React.Component<
const deltaTime = now - lastTick;
const deltaSec = deltaTime / 1000;
this.setState({
const cpu = Math.round(ips / deltaTime / 1000);
const diskRead = Math.round(this.diskReadBytes / deltaSec);
const diskWrite = Math.round(this.diskWriteBytes / deltaSec);
const netRx = Math.round(this.netRxBytes / deltaSec);
const netTx = Math.round(this.netTxBytes / deltaSec);
const push = (arr: number[], v: number) => [...arr, v].slice(-HISTORY_LEN);
this.setState((s) => ({
lastTick: now,
lastCounter: instructionCounter,
cpu: Math.round(ips / deltaTime / 1000),
diskRead: Math.round(this.diskReadBytes / deltaSec),
diskWrite: Math.round(this.diskWriteBytes / deltaSec),
netRx: Math.round(this.netRxBytes / deltaSec),
netTx: Math.round(this.netTxBytes / deltaSec),
});
cpu,
diskRead,
diskWrite,
netRx,
netTx,
history: {
cpu: push(s.history.cpu, cpu),
diskRead: push(s.history.diskRead, diskRead),
diskWrite: push(s.history.diskWrite, diskWrite),
netRx: push(s.history.netRx, netRx),
netTx: push(s.history.netTx, netTx),
},
}));
this.diskReadBytes = 0;
this.diskWriteBytes = 0;

View File

@@ -8,16 +8,30 @@ import { getDiskImageSize } from "../utils/disk-image-size";
import { CardStart } from "./card-start";
import { CardSettings } from "./card-settings";
import { EmulatorInfo } from "./emulator-info";
import { getStatePath } from "./utils/get-state-path";
import {
InfoBarSettings,
loadInfoBarSettings,
saveInfoBarSettings,
} from "./info-bar-settings";
import { getStatePath, getLegacyStatePath } from "./utils/get-state-path";
import { recoverLegacyDisk } from "./utils/recover-legacy-disk";
import { Win95Window } from "./app";
import { resetState } from "./utils/reset-state";
import { setupSmbShare } from "./smb";
import { setupTcpRelay } from "./net/tcp-relay";
import { setupTcpTrace } from "./net/tcp-trace";
import { setupDnsShim } from "./net/dns-shim";
import { setupClipboardSync } from "./clipboard";
import { startProbe } from "./debug-harness";
import { SyncFileBuffer } from "./sync-file-buffer";
const PROBE = process.env.WIN95_PROBE === "1";
const PROBE_OPTS: Record<string, unknown> = (() => {
try { return JSON.parse(process.env.WIN95_PROBE_OPTS || "{}"); }
catch { return {}; }
try {
return JSON.parse(process.env.WIN95_PROBE_OPTS || "{}");
} catch {
return {};
}
})();
declare let window: Win95Window;
@@ -31,13 +45,22 @@ export interface EmulatorState {
smbSharePath: string;
isBootingFresh: boolean;
isCursorCaptured: boolean;
hasAbsoluteMouse: boolean;
isInfoDisplayed: boolean;
isRunning: boolean;
infoBarSettings: InfoBarSettings;
legacyStatePath: string | null;
legacyRecovered: { dir: string; files: number } | null;
legacyRecoverBusy: boolean;
legacyRecoverError: string | null;
}
export class Emulator extends React.Component<{}, EmulatorState> {
private isQuitting = false;
private isResetting = false;
// Mirrors state.hasAbsoluteMouse but updated synchronously — setState is
// batched, and the lock/unlock decisions can't wait for a render.
private absoluteMouse = false;
constructor(props: {}) {
super(props);
@@ -51,10 +74,16 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.state = {
isBootingFresh: PROBE,
isCursorCaptured: false,
hasAbsoluteMouse: false,
isRunning: false,
legacyStatePath: null,
legacyRecovered: null,
legacyRecoverBusy: false,
legacyRecoverError: null,
currentUiCard: "start",
isInfoDisplayed: true,
smbSharePath: "",
infoBarSettings: loadInfoBarSettings(),
// We can start pretty large
// If it's too large, it'll just grow until it hits borders
scale: 2,
@@ -68,6 +97,8 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.setState({ smbSharePath: p });
});
getLegacyStatePath().then((p) => this.setState({ legacyStatePath: p }));
if (PROBE) {
// Skip the start card; boot fresh immediately. The 100ms delay
// lets React mount the #emulator div first.
@@ -98,21 +129,24 @@ export class Emulator extends React.Component<{}, EmulatorState> {
// Click
document.addEventListener("click", () => {
const { isRunning } = this.state;
if (isRunning) {
if (this.state.isRunning && !this.absoluteMouse) {
this.lockMouse();
}
});
// Only forward mouse input to the VM while the pointer is actually
// captured. Browsers can release pointer lock on their own (Esc, focus
// loss), so we sync v86's mouse status off the real lock state instead of
// captured (or while the guest's absolute-pointer driver is active —
// VBMOUSE/vmwmouse via the VMware backdoor — in which case the guest
// cursor tracks the host cursor 1:1 and we don't need pointer lock at
// all). Browsers can release pointer lock on their own (Esc, focus loss),
// so we sync v86's mouse status off the real lock state instead of
// assuming our lock/unlock calls succeeded.
document.addEventListener("pointerlockchange", () => {
const isCursorCaptured = !!document.pointerLockElement;
this.setState({ isCursorCaptured });
this.state.emulator?.mouse_set_status(isCursorCaptured);
this.state.emulator?.mouse_set_status(
isCursorCaptured || this.absoluteMouse,
);
});
}
@@ -181,6 +215,10 @@ export class Emulator extends React.Component<{}, EmulatorState> {
ipcRenderer.on(IPC_COMMANDS.MACHINE_STOP, this.stopEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESET, this.resetEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_START, this.startEmulator);
ipcRenderer.on(
IPC_COMMANDS.MACHINE_BOOT_FROM_SCRATCH,
this.bootFromScratch,
);
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESTART, this.restartEmulator);
ipcRenderer.on(IPC_COMMANDS.TOGGLE_INFO, () => {
@@ -235,12 +273,56 @@ export class Emulator extends React.Component<{}, EmulatorState> {
floppy={floppyFile}
cdrom={cdromFile}
smbSharePath={this.state.smbSharePath}
infoBarSettings={this.state.infoBarSettings}
setInfoBarSettings={(infoBarSettings) => {
this.setState({ infoBarSettings });
saveInfoBarSettings(infoBarSettings);
}}
navigate={navigate}
/>
);
} else {
card = (
<CardStart startEmulator={this.startEmulator} navigate={navigate} />
<CardStart
startEmulator={this.startEmulator}
navigate={navigate}
legacyStatePath={this.state.legacyStatePath}
legacyRecovered={this.state.legacyRecovered}
legacyRecoverBusy={this.state.legacyRecoverBusy}
legacyRecoverError={this.state.legacyRecoverError}
recoverLegacy={async () => {
const p = this.state.legacyStatePath;
if (!p) return;
this.setState({
legacyRecoverBusy: true,
legacyRecoverError: null,
});
try {
const downloads =
process.env.WIN95_RECOVER_DIR ||
(await ipcRenderer.invoke(IPC_COMMANDS.GET_DOWNLOADS_PATH));
const outDir = path.join(downloads, "Recovered C Drive");
const out = await recoverLegacyDisk(p, outDir);
this.setState({ legacyRecovered: out });
} catch (e) {
console.error("recoverLegacy:", e);
this.setState({
legacyRecoverError: e instanceof Error ? e.message : String(e),
});
} finally {
this.setState({ legacyRecoverBusy: false });
}
}}
showRecovered={() =>
this.state.legacyRecovered &&
shell.openPath(this.state.legacyRecovered.dir)
}
discardLegacy={async () => {
const p = this.state.legacyStatePath;
if (p) await fs.promises.unlink(p).catch(() => {});
this.setState({ legacyStatePath: null });
}}
/>
);
}
@@ -255,7 +337,10 @@ export class Emulator extends React.Component<{}, EmulatorState> {
<>
{this.renderInfo()}
{this.renderUI()}
<div id="emulator">
<div
id="emulator"
className={this.state.hasAbsoluteMouse ? "seamless-mouse" : undefined}
>
<div id="emulator-text-screen"></div>
<canvas id="emulator-canvas"></canvas>
</div>
@@ -270,6 +355,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
return (
<EmulatorInfo
emulator={this.state.emulator}
settings={this.state.infoBarSettings}
hidden={!this.state.isInfoDisplayed}
toggleInfo={() => {
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
@@ -281,7 +367,8 @@ export class Emulator extends React.Component<{}, EmulatorState> {
/**
* Boot the emulator without restoring state
*/
public bootFromScratch() {
public async bootFromScratch() {
await this.stopEmulator();
this.setState({ isBootingFresh: true });
this.startEmulator();
}
@@ -302,9 +389,11 @@ export class Emulator extends React.Component<{}, EmulatorState> {
private async startEmulator() {
document.body.classList.remove("paused");
const cdromPath = this.state.cdromFile
? webUtils.getPathForFile(this.state.cdromFile)
: null;
const cdromPath =
process.env.WIN95_PROBE_CDROM ||
(this.state.cdromFile
? webUtils.getPathForFile(this.state.cdromFile)
: null);
const options = {
wasm_path: path.join(__dirname, "build/v86.wasm"),
@@ -318,6 +407,9 @@ export class Emulator extends React.Component<{}, EmulatorState> {
net_device: {
relay_url: "fetch",
type: "ne2k",
// Real IPs for the guest so the raw-TCP relay knows where to dial;
// the default "static" resolver returns a placeholder for every name.
dns_method: "doh",
},
bios: {
url: path.join(__dirname, "../../bios/seabios.bin"),
@@ -335,13 +427,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
buffer: this.state.floppyFile,
}
: undefined,
cdrom: cdromPath
? {
url: cdromPath,
async: true,
size: await getDiskImageSize(cdromPath),
}
: undefined,
cdrom: cdromPath ? new SyncFileBuffer(cdromPath) : undefined,
boot_order: 0x132,
};
@@ -351,24 +437,44 @@ export class Emulator extends React.Component<{}, EmulatorState> {
console.log(`🚜 Starting emulator with options`, options);
// Answer the app's magic hostnames locally before v86's DoH path sends
// them to Cloudflare (which would NXDOMAIN them).
setupDnsShim();
window["emulator"] = new V86(options);
// Raw TCP egress for ports the fetch adapter ignores (everything but 80).
setupTcpRelay(window["emulator"]);
setupTcpTrace(window["emulator"]);
// Serve a host folder over SMB on port 139. Read-only, traversal/symlink
// guarded. In Win95: Start → Run → \\HOST\HOST. The env var wins so the
// probe harness can point at a fixture dir without touching settings.
const smbRoot = process.env.WIN95_SMB_SHARE || this.state.smbSharePath;
if (smbRoot) {
setupSmbShare(window["emulator"], smbRoot);
setupSmbShare(window["emulator"], smbRoot, CONSTANTS.TOOLS_PATH);
}
if (PROBE) {
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 the pointerlockchange listener confirms the
// cursor is actually captured.
// Mouse stays disabled until either the pointer is captured or the guest's
// VMware-backdoor mouse driver requests absolute mode.
window["emulator"].mouse_set_status(false);
window["emulator"].add_listener("vmware-absolute-mouse", (on: boolean) => {
this.absoluteMouse = on;
this.setState({ hasAbsoluteMouse: on });
window["emulator"].mouse_set_status(on || !!document.pointerLockElement);
if (on && document.pointerLockElement) {
this.unlockMouse();
}
});
this.setState({
emulator: window["emulator"],
isRunning: true,
@@ -381,10 +487,9 @@ export class Emulator extends React.Component<{}, EmulatorState> {
// wait time (lol)
setTimeout(async () => {
if (!this.state.isBootingFresh) {
this.restoreState();
await this.restoreState();
}
this.lockMouse();
this.state.emulator.run();
this.state.emulator.screen_set_scale(this.state.scale);
}, 500);
@@ -462,36 +567,36 @@ export class Emulator extends React.Component<{}, EmulatorState> {
/**
* Restores state to the emulator.
*/
private async restoreState() {
private async restoreState(): Promise<boolean> {
const { emulator, isBootingFresh } = this.state;
const state = await this.getState();
if (isBootingFresh) {
console.log(`restoreState: Booting fresh, not restoring.`);
return;
return true;
} else if (!state) {
console.log(`restoreState: No state present, not restoring.`);
return;
return false;
} else if (!emulator) {
console.log(`restoreState: No emulator present`);
return;
return false;
}
try {
await this.state.emulator.restore_state(state);
return true;
} catch (error) {
console.log(
`restoreState: Could not read state file. Maybe none exists?`,
error,
);
return false;
}
}
/**
* Returns the current machine's state - either what
* we have saved or alternatively the default state.
*
* @returns {ArrayBuffer}
*/
private async getState(): Promise<ArrayBuffer | null> {
const expectedStatePath = await getStatePath();

View File

@@ -0,0 +1,29 @@
export interface InfoBarSettings {
showCpu: boolean;
showDisk: boolean;
showNet: boolean;
showSparklines: boolean;
}
export const DEFAULT_INFO_BAR_SETTINGS: InfoBarSettings = {
showCpu: true,
showDisk: true,
showNet: true,
showSparklines: true,
};
const KEY = "infoBarSettings";
export function loadInfoBarSettings(): InfoBarSettings {
try {
const raw = localStorage.getItem(KEY);
if (raw) return { ...DEFAULT_INFO_BAR_SETTINGS, ...JSON.parse(raw) };
} catch {}
return { ...DEFAULT_INFO_BAR_SETTINGS };
}
export function saveInfoBarSettings(s: InfoBarSettings) {
try {
localStorage.setItem(KEY, JSON.stringify(s));
} catch {}
}

View File

@@ -11,21 +11,21 @@ e.setRequestHeader("Range","bytes="+f+"-"+(f+b.range.length-1)),e.setRequestHead
l&&"identity"!==l&&console.error("Server sent Content-Encoding in response to ranged request",{filename:a,enc:l})}b.done&&b.done(e.response,e)}};e.onerror=function(l){console.error("Loading the image "+a+" failed",l);d()};b.progress&&(e.onprogress=function(l){b.progress(l)});e.send(null)},pa=async function(a){return new Promise((b,c)=>{oa(a,{done:(d,e)=>{d=e.getResponseHeader("Content-Range")||"";(e=d.match(/\/(\d+)\s*$/))?b(+e[1]):c(Error("`Range: bytes=...` header not supported (Got `"+d+"`)"))},
headers:{Range:"bytes=0-0","X-Accept-Encoding":"identity"}})})};function qa(a,b,c){return String.fromCharCode(...(new Uint8Array(a.buffer,b>>>0,c>>>0)))}
const ra={cp437:" \u263a\u263b\u2665\u2666\u2663\u2660\u2022\u25d8\u25cb\u25d9\u2642\u2640\u266a\u266b\u263c\u25ba\u25c4\u2195\u203c\u00b6\u00a7\u25ac\u21a8\u2191\u2193\u2192\u2190\u221f\u2194\u25b2\u25bc !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u2302\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00a2\u00a3\u00a5\u20a7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u2310\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u00df\u0393\u03c0\u03a3\u03c3\u00b5\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u03c6\u03b5\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0 ",cp858:"\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u00c1\u00c2\u00c0\u00a9\u2563\u2551\u2557\u255d\u00a2\u00a5\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u00e3\u00c3\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u00a4\u00f0\u00d0\u00ca\u00cb\u00c8\u20ac\u00cd\u00ce\u00cf\u2518\u250c\u2588\u2584\u00a6\u00cc\u2580\u00d3\u00df\u00d4\u00d2\u00f5\u00d5\u00b5\u00fe\u00de\u00da\u00db\u00d9\u00fd\u00dd\u00af\u00b4\u00ad\u00b1\u2017\u00be\u00b6\u00a7\u00f7\u00b8\u00b0\u00a8\u00b7\u00b9\u00b3\u00b2\u25a0 "};
ra.cp858=ra.cp437.slice(0,128)+ra.cp858;ra.ascii=ra.cp437.split("").map((a,b)=>31<b&&128>b?a:".").join("");function sa(a){return a&&ra[a]?ra[a]:ra.cp437};function ua(){}function va(){};function wa(a,b){function c(w){w=w.toString(16);return"#"+"0".repeat(6-w.length)+w}function d(w){var u=256*na,H=8*R,K=Ta?Ta.canvas:null;K&&K.width===u&&K.height===H||(K?(K.width=u,K.height=H):(K=new OffscreenCanvas(u,H),Ta=K.getContext("2d")),Kb=Ta.createImageData(u,H));const Q=Kb.data;let P=0,S;H=Lb?function(Z){S=S||Z;Q[P+3]=Z;Q[P+7]=Z;P+=8}:function(Z){S=S||Z;Q[P+3]=Z;P+=4};K=32-R;const ja=u*(R-1)*4;u=4*(na-u*R);const fa=1020*na;for(let Z=0,Ea=0;2048>Z;++Z,Ea+=K,P+=u){const Fa=Z%256;Z&&!Fa&&(P+=
ja);S=!1;for(let La=0;La<R;++La,++Ea,P+=fa){const Ma=w[Ea];for(let ta=128;0<ta;ta>>=1)H(Ma&ta?255:0);Mb&&H(Nb&&192<=Fa&&223>=Fa&&Ma&1?255:0)}fc[Z]=S?1:0}Ta.putImageData(Kb,0,0)}function e(w,u,H,K){if(u&&H){w.style.width="";w.style.height="";K&&(w.style.transform="");var Q=w.getBoundingClientRect();K?w.style.transform=(1===u?"":" scaleX("+u+")")+(1===H?"":" scaleY("+H+")"):(0===u%1&&0===H%1?(g.style.imageRendering="crisp-edges",g.style.imageRendering="pixelated",g.style["-ms-interpolation-mode"]="nearest-neighbor"):
ra.cp858=ra.cp437.slice(0,128)+ra.cp858;ra.ascii=ra.cp437.split("").map((a,b)=>31<b&&128>b?a:".").join("");function sa(a){return a&&ra[a]?ra[a]:ra.cp437};function ua(){}function va(){};function wa(a,b){function c(w){w=w.toString(16);return"#"+"0".repeat(6-w.length)+w}function d(w){var u=256*na,H=8*R,K=Ta?Ta.canvas:null;K&&K.width===u&&K.height===H||(K?(K.width=u,K.height=H):(K=new OffscreenCanvas(u,H),Ta=K.getContext("2d")),Lb=Ta.createImageData(u,H));const Q=Lb.data;let P=0,S;H=Mb?function(Z){S=S||Z;Q[P+3]=Z;Q[P+7]=Z;P+=8}:function(Z){S=S||Z;Q[P+3]=Z;P+=4};K=32-R;const ja=u*(R-1)*4;u=4*(na-u*R);const fa=1020*na;for(let Z=0,Ea=0;2048>Z;++Z,Ea+=K,P+=u){const Fa=Z%256;Z&&!Fa&&(P+=
ja);S=!1;for(let La=0;La<R;++La,++Ea,P+=fa){const Ma=w[Ea];for(let ta=128;0<ta;ta>>=1)H(Ma&ta?255:0);Nb&&H(Ob&&192<=Fa&&223>=Fa&&Ma&1?255:0)}gc[Z]=S?1:0}Ta.putImageData(Lb,0,0)}function e(w,u,H,K){if(u&&H){w.style.width="";w.style.height="";K&&(w.style.transform="");var Q=w.getBoundingClientRect();K?w.style.transform=(1===u?"":" scaleX("+u+")")+(1===H?"":" scaleY("+H+")"):(0===u%1&&0===H%1?(g.style.imageRendering="crisp-edges",g.style.imageRendering="pixelated",g.style["-ms-interpolation-mode"]="nearest-neighbor"):
(g.style.imageRendering="",g.style["-ms-interpolation-mode"]=""),K=window.devicePixelRatio||1,0!==K%1&&(u/=K,H/=K));1!==u&&(w.style.width=Q.width*u+"px");1!==H&&(w.style.height=Q.height*H+"px")}}const f=a.container;this.screen_fill_buffer=b;console.assert(f,"options.container must be provided");this.FLAG_BLINKING=1;this.FLAG_FONT_PAGE_B=2;let g=f.getElementsByTagName("canvas")[0];g||(g=document.createElement("canvas"),f.appendChild(g));const h=g.getContext("2d",{alpha:!1});let l=f.getElementsByTagName("div")[0];
l||(l=document.createElement("div"),f.appendChild(l));const m=document.createElement("div");var n,p,q=void 0!==a.scale?a.scale:1,r=void 0!==a.scale?a.scale:1,x=1,C,t,A,M,v,L,T,Ta,Kb,fc=new Int8Array(2048),R,na,Mb,Lb,Nb,Ob=0,Pb=0,sb,gc=0,tb,Qb,ub,Rb=sa(a.encoding),vb=0,Sb=!1;this.init=function(){m.classList.add("cursor");m.style.position="absolute";m.style.backgroundColor="#ccc";m.style.width="7px";m.style.display="inline-block";this.set_mode(!1);this.set_size_text(80,25);2===t&&this.set_size_graphical(720,
400,720,400);this.set_scale(q,r);this.timer()};this.make_screenshot=function(){const w=new Image;if(1===t||2===t)w.src=g.toDataURL("image/png");else{const u=[9,16],H=document.createElement("canvas");H.width=M*u[0];H.height=v*u[1];const K=H.getContext("2d");K.imageSmoothingEnabled=!1;K.font=window.getComputedStyle(l).font;K.textBaseline="top";for(let Q=0;Q<v;Q++)for(let P=0;P<M;P++){const S=4*(Q*M+P),ja=A[S+0],fa=A[S+3];K.fillStyle=c(A[S+2]);K.fillRect(P*u[0],Q*u[1],u[0],u[1]);K.fillStyle=c(fa);K.fillText(Rb[ja],
P*u[0],Q*u[1])}"none"!==m.style.display&&n<v&&p<M&&(K.fillStyle=m.style.backgroundColor,K.fillRect(p*u[0],n*u[1]+parseInt(m.style.marginTop,10),parseInt(m.style.width,10),parseInt(m.style.height,10)));w.src=H.toDataURL("image/png")}return w};this.put_char=function(w,u,H,K,Q,P){u=4*(w*M+u);A[u+0]=H;A[u+1]=K;A[u+2]=Q;A[u+3]=P;C[w]=1};this.timer=function(){vb=requestAnimationFrame(()=>this.update_screen())};this.update_screen=function(){Sb||(0===t?this.update_text():1===t?this.update_graphical():this.update_graphical_text());
this.timer()};this.update_text=function(){for(var w=0;w<v;w++)C[w]&&(this.text_update_row(w),C[w]=0)};this.update_graphical=function(){this.screen_fill_buffer()};this.update_graphical_text=function(){if(L){var w=performance.now();if(266<w-gc){sb=!sb;ub&&(C[n]=1);var u=4*M;for(let ja=0,fa=0;ja<v;++ja)if(C[ja])fa+=u;else for(var H=0;H<M;++H,fa+=4)if(A[fa+1]&1){C[ja]=1;fa+=u-4*H;break}gc=w}w=Ta.canvas;u=T.canvas;H=4*M;const Q=M*na,P=R;let S=0;for(let ja=0,fa=0,Z=0;ja<v;++ja,fa+=R){if(!C[ja]){Z+=H;continue}++S;
T.clearRect(0,P,Q,R);let Ea,Fa,La,Ma;for(let ta=0;ta<Q;ta+=na,Z+=4){const hc=A[Z+0];var K=A[Z+1];const ic=A[Z+2],jc=A[Z+3],kc=K&2?Pb:Ob;K=(!(K&1)||sb)&&fc[(kc<<8)+hc];La!==ic&&(void 0!==La&&(L.fillStyle=c(La),L.fillRect(Ma,fa,ta-Ma,R)),La=ic,Ma=ta);Ea!==jc&&(void 0!==Ea&&(T.fillStyle=c(Ea),T.fillRect(Fa,0,ta-Fa,R)),Ea=jc,Fa=ta);K&&T.drawImage(w,hc*na,kc*R,na,R,ta,P,na,R)}T.fillStyle=c(Ea);T.fillRect(Fa,0,Q-Fa,R);T.globalCompositeOperation="destination-in";T.drawImage(u,0,P,Q,R,0,0,Q,R);T.globalCompositeOperation=
"source-over";L.fillStyle=c(La);L.fillRect(Ma,fa,Q-Ma,R);L.drawImage(u,0,0,Q,R,0,fa,Q,R)}S&&(sb&&ub&&C[n]&&(L.fillStyle=c(A[4*(n*M+p)+3]),L.fillRect(p*na,n*R+tb,na,Qb-tb+1)),C.fill(0));S&&h.drawImage(L.canvas,0,0)}};this.destroy=function(){vb&&(cancelAnimationFrame(vb),vb=0)};this.pause=function(){Sb=!0;m.classList.remove("blinking-cursor")};this.continue=function(){Sb=!1;m.classList.add("blinking-cursor")};this.set_mode=function(w){t=w?1:a.use_graphical_text?2:0;0===t?(l.style.display="block",g.style.display=
"none"):(l.style.display="none",g.style.display="block",2===t&&C&&C.fill(1))};this.set_font_bitmap=function(w,u,H,K,Q,P){const S=H?16:u?9:8;if(R!==w||na!==S||Mb!==u||Lb!==H||Nb!==K||P)P=na!==S||R!==w,R=w,na=S,Mb=u,Lb=H,Nb=K,2===t&&(d(Q),C.fill(1),P&&this.set_size_graphical_text())};this.set_font_page=function(w,u){if(Ob!==w||Pb!==u)Ob=w,Pb=u,C.fill(1)};this.clear_screen=function(){h.fillStyle="#000";h.fillRect(0,0,g.width,g.height)};this.set_size_graphical_text=function(){if(Ta){var w=na*M,u=R*v,
l||(l=document.createElement("div"),f.appendChild(l));const m=document.createElement("div");var n,p,q=void 0!==a.scale?a.scale:1,r=void 0!==a.scale?a.scale:1,x=1,C,t,A,M,v,L,T,Ta,Lb,gc=new Int8Array(2048),R,na,Nb,Mb,Ob,Pb=0,Qb=0,tb,hc=0,ub,Rb,vb,Sb=sa(a.encoding),wb=0,Tb=!1;this.init=function(){m.classList.add("cursor");m.style.position="absolute";m.style.backgroundColor="#ccc";m.style.width="7px";m.style.display="inline-block";this.set_mode(!1);this.set_size_text(80,25);2===t&&this.set_size_graphical(720,
400,720,400);this.set_scale(q,r);this.timer()};this.make_screenshot=function(){const w=new Image;if(1===t||2===t)w.src=g.toDataURL("image/png");else{const u=[9,16],H=document.createElement("canvas");H.width=M*u[0];H.height=v*u[1];const K=H.getContext("2d");K.imageSmoothingEnabled=!1;K.font=window.getComputedStyle(l).font;K.textBaseline="top";for(let Q=0;Q<v;Q++)for(let P=0;P<M;P++){const S=4*(Q*M+P),ja=A[S+0],fa=A[S+3];K.fillStyle=c(A[S+2]);K.fillRect(P*u[0],Q*u[1],u[0],u[1]);K.fillStyle=c(fa);K.fillText(Sb[ja],
P*u[0],Q*u[1])}"none"!==m.style.display&&n<v&&p<M&&(K.fillStyle=m.style.backgroundColor,K.fillRect(p*u[0],n*u[1]+parseInt(m.style.marginTop,10),parseInt(m.style.width,10),parseInt(m.style.height,10)));w.src=H.toDataURL("image/png")}return w};this.put_char=function(w,u,H,K,Q,P){u=4*(w*M+u);A[u+0]=H;A[u+1]=K;A[u+2]=Q;A[u+3]=P;C[w]=1};this.timer=function(){wb=requestAnimationFrame(()=>this.update_screen())};this.update_screen=function(){Tb||(0===t?this.update_text():1===t?this.update_graphical():this.update_graphical_text());
this.timer()};this.update_text=function(){for(var w=0;w<v;w++)C[w]&&(this.text_update_row(w),C[w]=0)};this.update_graphical=function(){this.screen_fill_buffer()};this.update_graphical_text=function(){if(L){var w=performance.now();if(266<w-hc){tb=!tb;vb&&(C[n]=1);var u=4*M;for(let ja=0,fa=0;ja<v;++ja)if(C[ja])fa+=u;else for(var H=0;H<M;++H,fa+=4)if(A[fa+1]&1){C[ja]=1;fa+=u-4*H;break}hc=w}w=Ta.canvas;u=T.canvas;H=4*M;const Q=M*na,P=R;let S=0;for(let ja=0,fa=0,Z=0;ja<v;++ja,fa+=R){if(!C[ja]){Z+=H;continue}++S;
T.clearRect(0,P,Q,R);let Ea,Fa,La,Ma;for(let ta=0;ta<Q;ta+=na,Z+=4){const ic=A[Z+0];var K=A[Z+1];const jc=A[Z+2],kc=A[Z+3],lc=K&2?Qb:Pb;K=(!(K&1)||tb)&&gc[(lc<<8)+ic];La!==jc&&(void 0!==La&&(L.fillStyle=c(La),L.fillRect(Ma,fa,ta-Ma,R)),La=jc,Ma=ta);Ea!==kc&&(void 0!==Ea&&(T.fillStyle=c(Ea),T.fillRect(Fa,0,ta-Fa,R)),Ea=kc,Fa=ta);K&&T.drawImage(w,ic*na,lc*R,na,R,ta,P,na,R)}T.fillStyle=c(Ea);T.fillRect(Fa,0,Q-Fa,R);T.globalCompositeOperation="destination-in";T.drawImage(u,0,P,Q,R,0,0,Q,R);T.globalCompositeOperation=
"source-over";L.fillStyle=c(La);L.fillRect(Ma,fa,Q-Ma,R);L.drawImage(u,0,0,Q,R,0,fa,Q,R)}S&&(tb&&vb&&C[n]&&(L.fillStyle=c(A[4*(n*M+p)+3]),L.fillRect(p*na,n*R+ub,na,Rb-ub+1)),C.fill(0));S&&h.drawImage(L.canvas,0,0)}};this.destroy=function(){wb&&(cancelAnimationFrame(wb),wb=0)};this.pause=function(){Tb=!0;m.classList.remove("blinking-cursor")};this.continue=function(){Tb=!1;m.classList.add("blinking-cursor")};this.set_mode=function(w){t=w?1:a.use_graphical_text?2:0;0===t?(l.style.display="block",g.style.display=
"none"):(l.style.display="none",g.style.display="block",2===t&&C&&C.fill(1))};this.set_font_bitmap=function(w,u,H,K,Q,P){const S=H?16:u?9:8;if(R!==w||na!==S||Nb!==u||Mb!==H||Ob!==K||P)P=na!==S||R!==w,R=w,na=S,Nb=u,Mb=H,Ob=K,2===t&&(d(Q),C.fill(1),P&&this.set_size_graphical_text())};this.set_font_page=function(w,u){if(Pb!==w||Qb!==u)Pb=w,Qb=u,C.fill(1)};this.clear_screen=function(){h.fillStyle="#000";h.fillRect(0,0,g.width,g.height)};this.set_size_graphical_text=function(){if(Ta){var w=na*M,u=R*v,
H=2*R;L&&L.canvas.width===w&&L.canvas.height===u&&T.canvas.height===H||(L?(L.canvas.width=w,L.canvas.height=u,T.canvas.width=w,T.canvas.height=H):(L=(new OffscreenCanvas(w,u)).getContext("2d",{alpha:!1}),T=(new OffscreenCanvas(w,H)).getContext("2d")),this.set_size_graphical(w,u,w,u),C.fill(1))}};this.set_size_text=function(w,u){if(w!==M||u!==v)if(C=new Int8Array(u),A=new Int32Array(w*u*4),M=w,v=u,0===t){for(;l.childNodes.length>u;)l.removeChild(l.firstChild);for(;l.childNodes.length<u;)l.appendChild(document.createElement("div"));
for(w=0;w<u;w++)this.text_update_row(w);e(l,q,r,!0)}else 2===t&&this.set_size_graphical_text()};this.set_size_graphical=function(w,u){g.style.display="block";g.width=w;g.height=u;h.imageSmoothingEnabled=!1;x=640>=w&&2*w<window.innerWidth*window.devicePixelRatio&&2*u<window.innerHeight*window.devicePixelRatio?2:1;e(g,q*x,r*x,!1)};this.set_scale=function(w,u){q=w;r=u;e(l,q,r,!0);e(g,q*x,r*x,!1)};this.update_cursor_scanline=function(w,u,H){if(w!==tb||u!==Qb||H!==ub)0===t?H?(m.style.display="inline",
m.style.height=u-w+"px",m.style.marginTop=w+"px"):m.style.display="none":2===t&&n<v&&(C[n]=1),tb=w,Qb=u,ub=H};this.update_cursor=function(w,u){if(w!==n||u!==p)w<v&&(C[w]=1),n<v&&(C[n]=1),n=w,p=u};this.text_update_row=function(w){var u=4*w*M,H;var K=l.childNodes[w];var Q=document.createElement("div");for(var P=0;P<M;){var S=document.createElement("span");var ja=A[u+1]&1;var fa=A[u+2];var Z=A[u+3];ja&&S.classList.add("blink");S.style.backgroundColor=c(fa);S.style.color=c(Z);for(H="";P<M&&(A[u+1]&1)===
ja&&A[u+2]===fa&&A[u+3]===Z;)if(H+=Rb[A[u+0]],P++,u+=4,w===n)if(P===p)break;else if(P===p+1){m.style.backgroundColor=S.style.color;Q.appendChild(m);break}S.textContent=H;Q.appendChild(S)}K.parentNode.replaceChild(Q,K)};this.update_buffer=function(w){for(const u of w)h.putImageData(u.image_data,u.screen_x-u.buffer_x,u.screen_y-u.buffer_y,u.buffer_x,u.buffer_y,u.buffer_width,u.buffer_height)};this.get_text_screen=function(){for(var w=[],u=0;u<v;u++)w.push(this.get_text_row(u));return w};this.get_text_row=
function(w){var u=w*M*4;w=u+4*M;let H="";for(;u<w;u+=4)H+=Rb[A[u]];return H};this.init()};function z(a){this.buffer=a;this.byteLength=a.byteLength;this.onprogress=this.onload=void 0}z.prototype.load=function(){this.onload&&this.onload({buffer:this.buffer})};z.prototype.get=function(a,b,c){c(new Uint8Array(this.buffer,a,b))};z.prototype.set=function(a,b,c){(new Uint8Array(this.buffer,a,b.byteLength)).set(b);c()};z.prototype.get_buffer=function(a){a(this.buffer)};z.prototype.get_state=function(){const a=[];a[0]=this.byteLength;a[1]=new Uint8Array(this.buffer);return a};
for(w=0;w<u;w++)this.text_update_row(w);e(l,q,r,!0)}else 2===t&&this.set_size_graphical_text()};this.set_size_graphical=function(w,u){g.style.display="block";g.width=w;g.height=u;h.imageSmoothingEnabled=!1;x=640>=w&&2*w<window.innerWidth*window.devicePixelRatio&&2*u<window.innerHeight*window.devicePixelRatio?2:1;e(g,q*x,r*x,!1)};this.set_scale=function(w,u){q=w;r=u;e(l,q,r,!0);e(g,q*x,r*x,!1)};this.update_cursor_scanline=function(w,u,H){if(w!==ub||u!==Rb||H!==vb)0===t?H?(m.style.display="inline",
m.style.height=u-w+"px",m.style.marginTop=w+"px"):m.style.display="none":2===t&&n<v&&(C[n]=1),ub=w,Rb=u,vb=H};this.update_cursor=function(w,u){if(w!==n||u!==p)w<v&&(C[w]=1),n<v&&(C[n]=1),n=w,p=u};this.text_update_row=function(w){var u=4*w*M,H;var K=l.childNodes[w];var Q=document.createElement("div");for(var P=0;P<M;){var S=document.createElement("span");var ja=A[u+1]&1;var fa=A[u+2];var Z=A[u+3];ja&&S.classList.add("blink");S.style.backgroundColor=c(fa);S.style.color=c(Z);for(H="";P<M&&(A[u+1]&1)===
ja&&A[u+2]===fa&&A[u+3]===Z;)if(H+=Sb[A[u+0]],P++,u+=4,w===n)if(P===p)break;else if(P===p+1){m.style.backgroundColor=S.style.color;Q.appendChild(m);break}S.textContent=H;Q.appendChild(S)}K.parentNode.replaceChild(Q,K)};this.update_buffer=function(w){for(const u of w)h.putImageData(u.image_data,u.screen_x-u.buffer_x,u.screen_y-u.buffer_y,u.buffer_x,u.buffer_y,u.buffer_width,u.buffer_height)};this.get_text_screen=function(){for(var w=[],u=0;u<v;u++)w.push(this.get_text_row(u));return w};this.get_text_row=
function(w){var u=w*M*4;w=u+4*M;let H="";for(;u<w;u+=4)H+=Sb[A[u]];return H};this.init()};function z(a){this.buffer=a;this.byteLength=a.byteLength;this.onprogress=this.onload=void 0}z.prototype.load=function(){this.onload&&this.onload({buffer:this.buffer})};z.prototype.get=function(a,b,c){c(new Uint8Array(this.buffer,a,b))};z.prototype.set=function(a,b,c){(new Uint8Array(this.buffer,a,b.byteLength)).set(b);c()};z.prototype.get_buffer=function(a){a(this.buffer)};z.prototype.get_state=function(){const a=[];a[0]=this.byteLength;a[1]=new Uint8Array(this.buffer);return a};
z.prototype.set_state=function(a){this.byteLength=a[0];this.buffer=a[1].slice().buffer};function xa(a,b,c){this.filename=a;this.byteLength=b;this.block_cache=new Map;this.block_cache_is_write=new Set;this.fixed_chunk_size=c;this.cache_reads=!!c;this.onprogress=this.onload=void 0}xa.prototype.load=async function(){void 0===this.byteLength&&(this.byteLength=await pa(this.filename));this.onload&&this.onload(Object.create(null))};
xa.prototype.get_from_cache=function(a,b){var c=b/256;a/=256;for(var d=0;d<c;d++)if(!this.block_cache.get(a+d))return;if(1===c)return this.block_cache.get(a);b=new Uint8Array(b);for(d=0;d<c;d++)b.set(this.block_cache.get(a+d),256*d);return b};
xa.prototype.get=function(a,b,c){var d=this.get_from_cache(a,b);if(d)c(d);else{var e=a,f=b;this.fixed_chunk_size&&(e=a-a%this.fixed_chunk_size,f=Math.ceil((a-e+b)/this.fixed_chunk_size)*this.fixed_chunk_size);oa(this.filename,{done:function(g){g=new Uint8Array(g);this.handle_read(e,f,g);e===a&&f===b?c(g):c(g.subarray(a-e,a-e+b))}.bind(this),range:{start:e,length:f}})}};
@@ -170,7 +170,7 @@ kb.prototype.queue=function(a){var b=a[0].length,c=b/this.sampling_rate;if(1<thi
a=this.audio_context.createBufferSource();a.buffer=d;a.connect(this.node_lowpass);a.addEventListener("ended",this.pump.bind(this));d=this.audio_context.currentTime;if(this.buffered_time<d)for(this.buffered_time=d,d=.2-c,b=0;b<=d;)b+=c,this.buffered_time+=c,setTimeout(()=>this.pump(),1E3*b);a.start(this.buffered_time);this.buffered_time+=c;setTimeout(()=>this.pump(),0)};kb.prototype.pump=function(){this.enabled&&(.2<this.buffered_time-this.audio_context.currentTime||this.bus.send("dac-request-data"))};function ob(a,b,c){this.bus=b;this.socket=void 0;this.id=c||0;this.send_queue=[];this.url=a;this.reconnect_interval=1E4;this.last_connect_attempt=Date.now()-this.reconnect_interval;this.send_queue_limit=64;this.destroyed=!1;this.bus.register("net"+this.id+"-send",function(d){this.send(d)},this)}ob.prototype.handle_message=function(a){this.bus&&this.bus.send("net"+this.id+"-receive",new Uint8Array(a.data))};
ob.prototype.handle_close=function(){this.destroyed||(this.connect(),setTimeout(this.connect.bind(this),this.reconnect_interval))};ob.prototype.handle_open=function(){for(var a=0;a<this.send_queue.length;a++)this.send(this.send_queue[a]);this.send_queue=[]};ob.prototype.handle_error=function(){};ob.prototype.destroy=function(){this.destroyed=!0;this.socket&&this.socket.close()};
ob.prototype.connect=function(){if("undefined"!==typeof WebSocket){if(this.socket){var a=this.socket.readyState;if(0===a||1===a)return}a=Date.now();if(!(this.last_connect_attempt+this.reconnect_interval>a)){this.last_connect_attempt=Date.now();try{this.socket=new WebSocket(this.url)}catch(b){console.error(b);return}this.socket.binaryType="arraybuffer";this.socket.onopen=this.handle_open.bind(this);this.socket.onmessage=this.handle_message.bind(this);this.socket.onclose=this.handle_close.bind(this);
this.socket.onerror=this.handle_error.bind(this)}}};ob.prototype.send=function(a){this.socket&&1===this.socket.readyState?this.socket.send(a):(this.send_queue.push(a),this.send_queue.length>2*this.send_queue_limit&&(this.send_queue=this.send_queue.slice(-this.send_queue_limit)),this.connect())};ob.prototype.change_proxy=function(a){this.url=a;this.socket&&(this.socket.onclose=function(){},this.socket.onerror=function(){},this.socket.close(),this.socket=void 0)};const pb=(new Date("1970-01-01T00:00:00Z")).getTime(),qb=(new Date("1900-01-01T00:00:00Z")).getTime(),rb=pb-qb,wb=Math.pow(2,32),xb=[118,56,54];function yb(a){return[0,1,2,3,4,5].map(b=>a[b].toString(16)).map(b=>1===b.length?"0"+b:b).join(":")}function zb(a){return a[0]<<24|a[1]<<16|a[2]<<8|a[3]}
this.socket.onerror=this.handle_error.bind(this)}}};ob.prototype.send=function(a){this.socket&&1===this.socket.readyState?this.socket.send(a):(this.send_queue.push(a),this.send_queue.length>2*this.send_queue_limit&&(this.send_queue=this.send_queue.slice(-this.send_queue_limit)),this.connect())};ob.prototype.change_proxy=function(a){this.url=a;this.socket&&(this.socket.onclose=function(){},this.socket.onerror=function(){},this.socket.close(),this.socket=void 0)};const pb=(new Date("1970-01-01T00:00:00Z")).getTime(),qb=(new Date("1900-01-01T00:00:00Z")).getTime(),rb=pb-qb,sb=Math.pow(2,32),xb=[118,56,54];function yb(a){return[0,1,2,3,4,5].map(b=>a[b].toString(16)).map(b=>1===b.length?"0"+b:b).join(":")}function zb(a){return a[0]<<24|a[1]<<16|a[2]<<8|a[3]}
class Ab{constructor(a,b){a=Math.min(a,16);this.maximum_capacity=b?Math.max(b,a):0;this.length=this.head=this.tail=0;this.buffer=new Uint8Array(a)}write(a){const b=a.length;var c=this.length+b;let d=this.buffer.length;if(d<c){for(;d<c;)d*=2;if(this.maximum_capacity&&d>this.maximum_capacity)throw Error("stream capacity overflow in GrowableRingbuffer.write(), package dropped");c=new Uint8Array(d);this.peek(c);this.tail=0;this.head=this.length;this.buffer=c}c=this.buffer;const e=this.head+b;if(e>d){const f=
d-this.head;c.set(a.subarray(0,f),this.head);c.set(a.subarray(f))}else c.set(a,this.head);this.head=e%d;this.length+=b}peek(a){const b=Math.min(this.length,a.length);if(b){const e=this.buffer;var c=e.length,d=this.tail+b;d>c?(d%=c,c-=this.tail,a.set(e.subarray(this.tail)),a.set(e.subarray(0,d),c)):a.set(e.subarray(this.tail,d))}return b}remove(a){a>this.length&&(a=this.length);a&&(this.tail=(this.tail+a)%this.buffer.length,this.length-=a);return a}}
function Bb(a=1500){const b=a-20,c=b-8,d=new Uint8Array(14+a+4),e=d.buffer,f=d.byteOffset;return{eth_frame:d,eth_frame_view:new DataView(e),eth_payload_view:new DataView(e,f+14,a),ipv4_payload_view:new DataView(e,f+34,b),udp_payload_view:new DataView(e,f+42,c),text_encoder:new TextEncoder}}function Cb(a,b,c,d){d.eth_frame.set(b,c.byteOffset+a);return b.length}
@@ -191,16 +191,16 @@ function Hb(a,b){let c={};var d=(new DataView(a.buffer,a.byteOffset,a.byteLength
a.rst=!!(d&4),a.psh=!!(d&8),a.ack=!!(d&16),a.urg=!!(d&32),a.ece=!!(d&64),a.cwr=!!(d&128),c.tcp=a,c.tcp_data=e.subarray(4*a.doff);else if(17===d){a=new DataView(e.buffer,e.byteOffset,e.byteLength);a={sport:a.getUint16(0),dport:a.getUint16(2),len:a.getUint16(4),checksum:a.getUint16(6),data:e.subarray(8),data_s:(new TextDecoder).decode(e.subarray(8))};if(67===a.dport||67===a.sport){e=e.subarray(8);d=new DataView(e.buffer,e.byteOffset,e.byteLength);e.subarray(44,236);d={op:d.getUint8(0),htype:d.getUint8(1),
hlen:d.getUint8(2),hops:d.getUint8(3),xid:d.getUint32(4),secs:d.getUint16(8),flags:d.getUint16(10),ciaddr:d.getUint32(12),yiaddr:d.getUint32(16),siaddr:d.getUint32(20),giaddr:d.getUint32(24),chaddr:e.subarray(28,44),magic:d.getUint32(236),options:[]};e=e.subarray(240);for(l=0;l<e.length;++l)g=l,0!==e[l]&&(++l,h=e[l],l+=h,d.options.push(e.subarray(g,g+h+2)));c.dhcp=d;c.dhcp_options=d.options}else 53===a.dport||53===a.sport?Ib(e.subarray(8),c):123===a.dport&&(d=e.subarray(8),d=new DataView(d.buffer,
d.byteOffset,d.byteLength),c.ntp={flags:d.getUint8(0),stratum:d.getUint8(1),poll:d.getUint8(2),precision:d.getUint8(3),root_delay:d.getUint32(4),root_disp:d.getUint32(8),ref_id:d.getUint32(12),ref_ts_i:d.getUint32(16),ref_ts_f:d.getUint32(20),ori_ts_i:d.getUint32(24),ori_ts_f:d.getUint32(28),rec_ts_i:d.getUint32(32),rec_ts_f:d.getUint32(36),trans_ts_i:d.getUint32(40),trans_ts_f:d.getUint32(44)});c.udp=a}}else 2054===d?(d=new DataView(a.buffer,a.byteOffset,a.byteLength),a={htype:d.getUint16(0),ptype:d.getUint16(2),
oper:d.getUint16(6),sha:a.subarray(8,14),spa:a.subarray(14,18),tha:a.subarray(18,24),tpa:a.subarray(24,28)},c.arp=a):34525!==d&&y(d);if(c.ipv4)if(c.tcp)a:{a=`${c.ipv4.src.join(".")}:${c.tcp.sport}:${c.ipv4.dest.join(".")}:${c.tcp.dport}`;if(c.tcp.syn&&!c.tcp.ack){b.tcp_conn[a]&&delete b.tcp_conn[a];d=new Jb(b);d.state="syn-received";d.tuple=a;d.last=c;d.hsrc=c.eth.dest;d.psrc=c.ipv4.dest;d.sport=c.tcp.dport;d.hdest=c.eth.src;d.dport=c.tcp.sport;d.pdest=c.ipv4.src;b.bus.pair.send("tcp-connection",
oper:d.getUint16(6),sha:a.subarray(8,14),spa:a.subarray(14,18),tha:a.subarray(18,24),tpa:a.subarray(24,28)},c.arp=a):34525!==d&&y(d);if(c.ipv4)if(c.tcp)a:{a=`${c.ipv4.src.join(".")}:${c.tcp.sport}:${c.ipv4.dest.join(".")}:${c.tcp.dport}`;if(c.tcp.syn&&!c.tcp.ack){b.tcp_conn[a]&&delete b.tcp_conn[a];d=new Jb(b);d.state="syn-received";d.tuple=a;d.last=c;d.hsrc=new Uint8Array(c.eth.dest);d.psrc=new Uint8Array(c.ipv4.dest);d.sport=c.tcp.dport;d.hdest=new Uint8Array(c.eth.src);d.dport=c.tcp.sport;d.pdest=new Uint8Array(c.ipv4.src);b.bus.pair.send("tcp-connection",
d);if(b.on_tcp_connection)b.on_tcp_connection(d,c);if(b.tcp_conn[a])break a}if(b.tcp_conn[a])b.tcp_conn[a].process(c);else{a=c.tcp.ackn;if(c.tcp.fin||c.tcp.syn)a+=1;d={};d.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src};d.ipv4={proto:6,src:c.ipv4.dest,dest:c.ipv4.src};d.tcp={sport:c.tcp.dport,dport:c.tcp.sport,seq:a,ackn:c.tcp.seq+(c.tcp.syn?1:0),winsize:c.tcp.winsize,rst:!0,ack:c.tcp.syn};b.receive(Eb(b.eth_encoder_buf,d))}}else if(c.udp)if(c.dns)if("static"===b.dns_method){a={};a.eth={ethertype:2048,
src:b.router_mac,dest:c.eth.src};a.ipv4={proto:17,src:b.router_ip,dest:c.ipv4.src};a.udp={sport:53,dport:c.udp.sport};d=[];for(e=0;e<c.dns.questions.length;++e)switch(l=c.dns.questions[e],l.type){case 1:d.push({name:l.name,type:l.type,class:l.class,ttl:600,data:[192,168,87,1]})}a.dns={id:c.dns.id,flags:33152,questions:c.dns.questions,answers:d};b.receive(Eb(b.eth_encoder_buf,a))}else Fb(c,b);else c.dhcp?Gb(c,b):c.ntp?(a=Date.now()+rb,d=a%1E3/1E3*wb,e={},e.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src},
src:b.router_mac,dest:c.eth.src};a.ipv4={proto:17,src:b.router_ip,dest:c.ipv4.src};a.udp={sport:53,dport:c.udp.sport};d=[];for(e=0;e<c.dns.questions.length;++e)switch(l=c.dns.questions[e],l.type){case 1:d.push({name:l.name,type:l.type,class:l.class,ttl:600,data:[192,168,87,1]})}a.dns={id:c.dns.id,flags:33152,questions:c.dns.questions,answers:d};b.receive(Eb(b.eth_encoder_buf,a))}else Fb(c,b);else c.dhcp?Gb(c,b):c.ntp?(a=Date.now()+rb,d=a%1E3/1E3*sb,e={},e.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src},
e.ipv4={proto:17,src:c.ipv4.dest,dest:c.ipv4.src},e.udp={sport:123,dport:c.udp.sport},e.ntp=Object.assign({},c.ntp),e.ntp.flags=36,e.ntp.poll=10,e.ntp.ori_ts_i=c.ntp.trans_ts_i,e.ntp.ori_ts_f=c.ntp.trans_ts_f,e.ntp.rec_ts_i=a/1E3,e.ntp.rec_ts_f=d,e.ntp.trans_ts_i=a/1E3,e.ntp.trans_ts_f=d,e.ntp.stratum=2,b.receive(Eb(b.eth_encoder_buf,e))):8===c.udp.dport&&(a={},a.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src},a.ipv4={proto:17,src:c.ipv4.dest,dest:c.ipv4.src},a.udp={sport:c.udp.dport,dport:c.udp.sport,
data:(new TextEncoder).encode(c.udp.data_s)},b.receive(Eb(b.eth_encoder_buf,a)));else c.icmp&&8===c.icmp.type&&(a={},a.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src},a.ipv4={proto:1,src:c.ipv4.dest,dest:c.ipv4.src},a.icmp={type:0,code:c.icmp.code,data:c.icmp.data},b.receive(Eb(b.eth_encoder_buf,a)));else c.arp&&1===c.arp.oper&&2048===c.arp.ptype&&(a=zb(c.arp.tpa)&4294967040,d=zb(b.router_ip)&4294967040,!b.masquerade&&a!==d||a===d&&99<c.arp.tpa[3]||(a={},a.eth={ethertype:2054,src:b.router_mac,
dest:c.eth.src},a.arp={htype:1,ptype:2048,oper:2,sha:b.router_mac,spa:c.arp.tpa,tha:c.eth.src,tpa:c.arp.spa},b.receive(Eb(b.eth_encoder_buf,a))))}
function Ib(a,b){function c(){let m=[],n;do n=d.getUint8(h),m.push((new TextDecoder).decode(a.subarray(h+1,h+1+n))),h+=n+1;while(0<n);return m}let d=new DataView(a.buffer,a.byteOffset,a.byteLength),e={id:d.getUint16(0),flags:d.getUint16(2),questions:[],answers:[]};var f=d.getUint16(4);let g=d.getUint16(6);d.getUint16(8);d.getUint16(10);let h=12;for(var l=0;l<f;l++)e.questions.push({name:c(),type:d.getInt16(h),class:d.getInt16(h+2)}),h+=4;for(f=0;f<g;f++){l={name:c(),type:d.getInt16(h),class:d.getUint16(h+
2),ttl:d.getUint32(h+4)};h+=8;let m=d.getUint16(h);h+=2;l.data=a.subarray(h,h+m);h+=m;e.answers.push(l)}b.dns=e}
function Tb(a,b){var c=b.vm_ip.join(".");const d=b.router_ip.join("."),e=16383*Math.random()|0;let f,g,h=0;do f=49152+(e+h)%16383,g=`${c}:${a}:${d}:${f}`;while(16383>++h&&b.tcp_conn[g]);if(b.tcp_conn[g])throw Error("pool of dynamic TCP port numbers exhausted, connection aborted");c=new Jb(b);c.tuple=g;c.hsrc=b.router_mac;c.psrc=b.router_ip;c.sport=f;c.hdest=b.vm_mac;c.dport=a;c.pdest=b.vm_ip;b.tcp_conn[g]=c;c.connect();return c}
function Ub(a,b){return new Promise(c=>{let d=Tb(a,b);d.state="syn-probe";d.on("probe",c)})}function Jb(a){this.mtu=a.mtu||1500;const b=this.mtu-20-20;this.state="closed";this.net=a;this.send_buffer=new Ab(2048,0);this.send_chunk_buf=new Uint8Array(b);this.delayed_send_fin=this.in_active_close=!1;this.delayed_state=void 0;this.events_handlers={}}Jb.prototype.on=function(a,b){this.events_handlers[a]=b};Jb.prototype.emit=function(a,...b){this.events_handlers[a]&&this.events_handlers[a].apply(this,b)};
function Kb(a,b){var c=b.vm_ip.join(".");const d=b.router_ip.join("."),e=16383*Math.random()|0;let f,g,h=0;do f=49152+(e+h)%16383,g=`${c}:${a}:${d}:${f}`;while(16383>++h&&b.tcp_conn[g]);if(b.tcp_conn[g])throw Error("pool of dynamic TCP port numbers exhausted, connection aborted");c=new Jb(b);c.tuple=g;c.hsrc=b.router_mac;c.psrc=b.router_ip;c.sport=f;c.hdest=b.vm_mac;c.dport=a;c.pdest=b.vm_ip;b.tcp_conn[g]=c;c.connect();return c}
function Ub(a,b){return new Promise(c=>{let d=Kb(a,b);d.state="syn-probe";d.on("probe",c)})}function Jb(a){this.mtu=a.mtu||1500;const b=this.mtu-20-20;this.state="closed";this.net=a;this.send_buffer=new Ab(2048,0);this.send_chunk_buf=new Uint8Array(b);this.delayed_send_fin=this.in_active_close=!1;this.delayed_state=void 0;this.events_handlers={}}Jb.prototype.on=function(a,b){this.events_handlers[a]=b};Jb.prototype.emit=function(a,...b){this.events_handlers[a]&&this.events_handlers[a].apply(this,b)};
Jb.prototype.ipv4_reply=function(){let a={};a.eth={ethertype:2048,src:this.hsrc,dest:this.hdest};a.ipv4={proto:6,src:this.psrc,dest:this.pdest};a.tcp={sport:this.sport,dport:this.dport,winsize:this.winsize,ackn:this.ack,seq:this.seq,ack:!0};return a};Jb.prototype.packet_reply=function(a,b){a={sport:a.tcp.dport,dport:a.tcp.sport,winsize:a.tcp.winsize,ackn:this.ack,seq:this.seq};if(b)for(const c in b)a[c]=b[c];b=this.ipv4_reply();b.tcp=a;return b};
Jb.prototype.connect=function(){this.seq=1338;this.ack=1;this.start_seq=0;this.winsize=64240;"syn-probe"!==this.state&&(this.state="syn-sent");let a=this.ipv4_reply();a.ipv4.id=2345;a.tcp={sport:this.sport,dport:this.dport,seq:1337,ackn:0,winsize:0,syn:!0};this.net.receive(Eb(this.net.eth_encoder_buf,a))};
Jb.prototype.accept=function(a){a=a||this.last;this.net.tcp_conn[this.tuple]=this;this.seq=1338;this.ack=a.tcp.seq+1;this.start_seq=a.tcp.seq;this.winsize=a.tcp.winsize;let b=this.ipv4_reply();b.tcp={sport:this.sport,dport:this.dport,seq:1337,ackn:this.ack,winsize:a.tcp.winsize,syn:!0,ack:!0,options:{mss:this.mtu-20-20}};this.state="established";this.net.receive(Eb(this.net.eth_encoder_buf,b))};
@@ -212,7 +212,7 @@ b)),this.emit("data",a.tcp_data));this.pump()}};Jb.prototype.write=function(a){t
Jb.prototype.close=function(){if(!this.in_active_close){this.in_active_close=!0;if("established"===this.state||"syn-received"===this.state)var a="fin-wait-1";else if("close-wait"===this.state)a="last-ack";else{this.release();return}this.send_buffer.length||this.pending?(this.delayed_send_fin=!0,this.delayed_state=a):(this.state=a,a=this.ipv4_reply(),a.tcp.fin=!0,this.net.receive(Eb(this.net.eth_encoder_buf,a)))}this.pump()};Jb.prototype.on_shutdown=function(){this.emit("shutdown")};
Jb.prototype.on_close=function(){this.emit("close")};Jb.prototype.release=function(){this.net.tcp_conn[this.tuple]&&(this.state="closed",delete this.net.tcp_conn[this.tuple])};Jb.prototype.pump=function(){if(this.send_buffer.length&&!this.pending){const a=this.send_chunk_buf,b=this.send_buffer.peek(a),c=this.ipv4_reply();c.tcp.psh=!0;c.tcp_data=a.subarray(0,b);this.net.receive(Eb(this.net.eth_encoder_buf,c));this.pending=!0}};function Vb(a,b){b=b||{};this.bus=a;this.id=b.id||0;this.router_mac=new Uint8Array((b.router_mac||"52:54:0:1:2:3").split(":").map(function(c){return parseInt(c,16)}));this.router_ip=new Uint8Array((b.router_ip||"192.168.86.1").split(".").map(function(c){return parseInt(c,10)}));this.vm_ip=new Uint8Array((b.vm_ip||"192.168.86.100").split(".").map(function(c){return parseInt(c,10)}));this.masquerade=void 0===b.masquerade||!!b.masquerade;this.vm_mac=new Uint8Array(6);this.dns_method=b.dns_method||"static";
this.doh_server=b.doh_server;this.tcp_conn={};this.mtu=b.mtu;this.eth_encoder_buf=Bb(this.mtu);this.fetch=(...c)=>fetch(...c);this.cors_proxy=b.cors_proxy;this.bus.register("net"+this.id+"-mac",function(c){this.vm_mac=new Uint8Array(c.split(":").map(function(d){return parseInt(d,16)}))},this);this.bus.register("net"+this.id+"-send",function(c){this.send(c)},this);this.bus.register("tcp-connection",c=>{80===c.sport&&(c.on("data",Wb),c.accept())},this)}Vb.prototype.destroy=function(){};
Vb.prototype.connect=function(a){return Tb(a,this)};Vb.prototype.tcp_probe=function(a){return Ub(a,this)};
Vb.prototype.connect=function(a){return Kb(a,this)};Vb.prototype.tcp_probe=function(a){return Ub(a,this)};
async function Wb(a){this.read=this.read||"";if((this.read+=(new TextDecoder).decode(a))&&-1!==this.read.indexOf("\r\n\r\n")){a=this.read.indexOf("\r\n\r\n");var b=this.read.substring(0,a).split(/\r\n/);a=this.read.substring(a+4);this.read="";let c=b[0].split(" "),d;d=/^https?:/.test(c[1])?new URL(c[1]):new URL("http://host"+c[1]);"undefined"!==typeof window&&"http:"===d.protocol&&"https:"===window.location.protocol&&(d.protocol="https:");let e=new Headers;for(let h=1;h<b.length;++h){const l=this.net.parse_http_header(b[h]);
if(!l){console.warn('The request contains an invalid header: "%s"',b[h]);this.net.respond_text_and_close(this,400,"Bad Request",`Invalid header in request: ${b[h]}`);return}"host"===l.key.toLowerCase()?d.host=l.value:e.append(l.key,l.value)}if(!this.net.cors_proxy&&/^\d+\.external$/.test(d.hostname))if(b=parseInt(d.hostname.split(".")[0],10),!isNaN(b)&&0<b&&65536>b)d.protocol="http:",d.hostname="localhost",d.port=b.toString(10);else{console.warn('Unknown port for localhost: "%s"',d.href);this.net.respond_text_and_close(this,
400,"Bad Request",`Unknown port for localhost: ${d.href}`);return}this.name=d.href;b={method:c[0],headers:e};-1!==["put","post"].indexOf(b.method.toLowerCase())&&(b.body=a);const f=this.net.cors_proxy?this.net.cors_proxy+encodeURIComponent(d.href):d.href;new TextEncoder;let g=!1;this.net.fetch(f,b).then(h=>{let l=new Headers(h.headers);l.delete("content-encoding");l.delete("keep-alive");l.delete("content-length");l.delete("transfer-encoding");l.set("x-was-fetch-redirected",`${!!h.redirected}`);l.set("x-fetch-resp-url",
@@ -250,9 +250,9 @@ b,!1);a.removeEventListener("keydown",c,!1);a.removeEventListener("paste",d,!1);
this.update())};this.update=function(){var g=Date.now(),h=g-this.last_update;16>h?void 0===this.update_timer&&(this.update_timer=setTimeout(()=>{this.update_timer=void 0;this.last_update=Date.now();this.render()},16-h)):(void 0!==this.update_timer&&(clearTimeout(this.update_timer),this.update_timer=void 0),this.last_update=g,this.render())};this.render=function(){a.value=this.text;this.text_new_line&&(this.text_new_line=!1,a.scrollTop=1E9)};this.send_char=function(){}}
function dc(a,b){var c=Reflect.construct(cc,[a],dc);c.send_char=function(d){b.send("serial0-input",d)};b.register("serial0-output-byte",function(d){d=String.fromCharCode(d);c.show_char&&c.show_char(d)},c);return c}Reflect.setPrototypeOf(dc.prototype,cc.prototype);Reflect.setPrototypeOf(dc,cc);
function ec(a,b){var c=Reflect.construct(cc,[a],ec);c.send_char=function(e){b.send("virtio-console0-input-bytes",new Uint8Array([e]))};const d=new TextDecoder;b.register("virtio-console0-output-bytes",function(e){for(const f of d.decode(e))c.show_char&&c.show_char(f)},c);return c}Reflect.setPrototypeOf(ec.prototype,cc.prototype);Reflect.setPrototypeOf(ec,cc);
function lc(a,b){this.element=a;var c=this.term=new b({logLevel:"off",convertEol:"true"});this.destroy=function(){this.on_data_disposable&&this.on_data_disposable.dispose();c.dispose()}}lc.prototype.show=function(){this.term&&this.term.open(this.element)};
function mc(a,b,c){if(c){var d=Reflect.construct(lc,[a,c],mc);b.register("serial0-output-byte",function(f){d.term.write(Uint8Array.of(f))},d);var e=new TextEncoder;d.on_data_disposable=d.term.onData(function(f){for(const g of e.encode(f))b.send("serial0-input",g)});return d}}Reflect.setPrototypeOf(mc.prototype,lc.prototype);Reflect.setPrototypeOf(mc,lc);
function nc(a,b,c){if(c){var d=Reflect.construct(lc,[a,c],nc);b.register("virtio-console0-output-bytes",function(f){d.term.write(f)},d);var e=new TextEncoder;d.on_data_disposable=d.term.onData(function(f){b.send("virtio-console0-input-bytes",e.encode(f))});return d}}Reflect.setPrototypeOf(nc.prototype,lc.prototype);Reflect.setPrototypeOf(nc,lc);function oc(a,b){b=b.id||0;this.bus=a;this.bus_send_msgid=`net${b}-send`;this.bus_recv_msgid=`net${b}-receive`;this.channel=new BroadcastChannel(`v86-inbrowser-${b}`);this.is_open=!0;this.nic_to_hub_fn=c=>{this.channel.postMessage(c)};this.bus.register(this.bus_send_msgid,this.nic_to_hub_fn,this);this.hub_to_nic_fn=c=>{this.bus.send(this.bus_recv_msgid,c.data)};this.channel.addEventListener("message",this.hub_to_nic_fn)}
function fc(a,b){this.element=a;var c=this.term=new b({logLevel:"off",convertEol:"true"});this.destroy=function(){this.on_data_disposable&&this.on_data_disposable.dispose();c.dispose()}}fc.prototype.show=function(){this.term&&this.term.open(this.element)};
function mc(a,b,c){if(c){var d=Reflect.construct(fc,[a,c],mc);b.register("serial0-output-byte",function(f){d.term.write(Uint8Array.of(f))},d);var e=new TextEncoder;d.on_data_disposable=d.term.onData(function(f){for(const g of e.encode(f))b.send("serial0-input",g)});return d}}Reflect.setPrototypeOf(mc.prototype,fc.prototype);Reflect.setPrototypeOf(mc,fc);
function nc(a,b,c){if(c){var d=Reflect.construct(fc,[a,c],nc);b.register("virtio-console0-output-bytes",function(f){d.term.write(f)},d);var e=new TextEncoder;d.on_data_disposable=d.term.onData(function(f){b.send("virtio-console0-input-bytes",e.encode(f))});return d}}Reflect.setPrototypeOf(nc.prototype,fc.prototype);Reflect.setPrototypeOf(nc,fc);function oc(a,b){b=b.id||0;this.bus=a;this.bus_send_msgid=`net${b}-send`;this.bus_recv_msgid=`net${b}-receive`;this.channel=new BroadcastChannel(`v86-inbrowser-${b}`);this.is_open=!0;this.nic_to_hub_fn=c=>{this.channel.postMessage(c)};this.bus.register(this.bus_send_msgid,this.nic_to_hub_fn,this);this.hub_to_nic_fn=c=>{this.bus.send(this.bus_recv_msgid,c.data)};this.channel.addEventListener("message",this.hub_to_nic_fn)}
oc.prototype.destroy=function(){this.is_open&&(this.bus.unregister(this.bus_send_msgid,this.nic_to_hub_fn),this.channel.removeEventListener("message",this.hub_to_nic_fn),this.channel.close(),this.is_open=!1)};function pc(){this.filedata=new Map}pc.prototype.read=async function(a,b,c){return(a=this.filedata.get(a))?a.subarray(b,b+c):null};pc.prototype.cache=async function(a,b){this.filedata.set(a,b)};pc.prototype.uncache=function(a){this.filedata.delete(a)};function qc(a,b,c){b.endsWith("/")||(b+="/");this.storage=a;this.baseurl=b;this.zstd_decompress=c}
qc.prototype.load_from_server=function(a,b){return new Promise(c=>{oa(this.baseurl+a,{done:async d=>{d=new Uint8Array(d);a.endsWith(".zst")&&(d=new Uint8Array(this.zstd_decompress(b,d)));await this.cache(a,d);c(d)}})})};qc.prototype.read=async function(a,b,c,d){const e=await this.storage.read(a,b,c,d);return e?e:(await this.load_from_server(a,d)).subarray(b,b+c)};qc.prototype.cache=async function(a,b){return await this.storage.cache(a,b)};qc.prototype.uncache=function(a){this.storage.uncache(a)};const rc=new TextDecoder,sc=new TextEncoder;
function G(a,b,c,d){for(var e,f=0,g=0;g<a.length;g++)switch(e=b[g],a[g]){case "w":c[d++]=e&255;c[d++]=e>>8&255;c[d++]=e>>16&255;c[d++]=e>>24&255;f+=4;break;case "d":c[d++]=e&255;c[d++]=e>>8&255;c[d++]=e>>16&255;c[d++]=e>>24&255;c[d++]=0;c[d++]=0;c[d++]=0;c[d++]=0;f+=8;break;case "h":c[d++]=e&255;c[d++]=e>>8;f+=2;break;case "b":c[d++]=e;f+=1;break;case "s":var h=d,l=0;c[d++]=0;c[d++]=0;f+=2;e=sc.encode(e);f+=e.byteLength;l+=e.byteLength;c.set(e,d);d+=e.byteLength;c[h+0]=l&255;c[h+1]=l>>8&255;break;
@@ -426,11 +426,17 @@ 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)}};const Hc=DataView.prototype,Ic={size:1,get:Hc.getUint8,set:Hc.setUint8},Jc={size:2,get:Hc.getUint16,set:Hc.setUint16},U={size:4,get:Hc.getUint32,set:Hc.setUint32},Lc=Kc([{magic:U},{class:Ic},{data:Ic},{version0:Ic},{osabi:Ic},{abiversion:Ic},{pad0:function(a){return{size:a,get:()=>-1}}(7)},{type:Jc},{machine:Jc},{version1:U},{entry:U},{phoff:U},{shoff:U},{flags:U},{ehsize:Jc},{phentsize:Jc},{phnum:Jc},{shentsize:Jc},{shnum:Jc},{shstrndx:Jc}]);console.assert(52===Lc.reduce((a,b)=>a+b.size,0));
const Mc=Kc([{type:U},{offset:U},{vaddr:U},{paddr:U},{filesz:U},{memsz:U},{flags:U},{align:U}]);console.assert(32===Mc.reduce((a,b)=>a+b.size,0));const Nc=Kc([{name:U},{type:U},{flags:U},{addr:U},{offset:U},{size:U},{link:U},{info:U},{addralign:U},{entsize:U}]);console.assert(40===Nc.reduce((a,b)=>a+b.size,0));function Kc(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<b.size);return{name:c,type:b,size:b.size,get:b.get,set:b.set}})}
function Oc(a,b){const c={};let d=0;for(const e of b)b=e.get.call(a,d,!0),console.assert(void 0===c[e.name]),c[e.name]=b,d+=e.size;return[c,d]}function Pc(a,b,c){const d=[];let e=0;for(var f=0;f<c;f++){const [g,h]=Oc(new DataView(a.buffer,a.byteOffset+e,void 0),b);d.push(g);e+=h}return[d,e]};const Qc={[0]:0,[1]:525,[2]:525,[3]:350,[4]:350,[5]:350};
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=65536<d.length?d.subarray(0,65536):d;this.clip_out_cursor=0;this.clip_out_fresh=!0},this);this.bus.register("mouse-absolute",function(d){const e=
Math.max(0,Math.min(65535,Math.round(d[0]/d[2]*65535)));d=Math.max(0,Math.min(65535,Math.round(d[1]/d[3]*65535)));if(e!==this.last_x||d!==this.last_y)this.last_x=e,this.last_y=d,this.push_packet(0,!0)},this);this.bus.register("mouse-click",function(d){this.buttons=(d[0]?32:0)|(d[1]?8:0)|(d[2]?16:0);this.push_packet(0,!1)},this);this.bus.register("mouse-wheel",function(d){this.push_packet(-d[0]|0,!1)},this);a.io.register_read(22104,this,function(){return 255},function(){return 65535},this.port_read32);
a.io.register_write(22104,this,c,c,c)}Hc.prototype.push_packet=function(a,b){!this.enabled||!this.absolute||0>this.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<this.queue.length+4?(this.enabled=!1,this.queue.length=0):(this.queue.push(this.buttons,this.last_x,this.last_y,a),this.tail_is_move=b))};
Hc.prototype.port_read32=function(){var a=this.cpu.reg32;if(1447909480!==a[0])return-1;switch(a[1]&65535){case 10:return a[3]=1447909480,6;case 6:if(!this.clip_out_fresh)break;this.clip_out_fresh=!1;this.clip_out_cursor=0;return this.clip_out.length;case 7:var b=this.clip_out;a=this.clip_out_cursor;b=b[a]|0|(b[a+1]|0)<<8|(b[a+2]|0)<<16|(b[a+3]|0)<<24;this.clip_out_cursor=a+4;return b;case 8:return a=Math.min(a[3]>>>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<b.length&&(b[c++]=a);c<b.length&&(b[c++]=a>>>8);c<b.length&&(b[c++]=a>>>16);c<b.length&&(b[c++]=a>>>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<b;d++)c[d]=this.queue.shift();a[3]=c[1];a[1]=c[2];a[2]=
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<b.size);return{name:c,type:b,size:b.size,get:b.get,set:b.set}})}
function Pc(a,b){const c={};let d=0;for(const e of b)b=e.get.call(a,d,!0),console.assert(void 0===c[e.name]),c[e.name]=b,d+=e.size;return[c,d]}function Qc(a,b,c){const d=[];let e=0;for(var f=0;f<c;f++){const [g,h]=Pc(new DataView(a.buffer,a.byteOffset+e,void 0),b);d.push(g);e+=h}return[d,e]};const Rc={[0]:0,[1]:525,[2]:525,[3]:350,[4]:350,[5]:350};
function V(a,b,c,d){this.io=a.io;this.cpu=a;this.dma=a.devices.dma;this.cmd_table=this.build_cmd_lookup_table();this.sra=0;this.srb=192;this.dor=12;this.tdr=0;this.msr=128;this.dsr=0;this.cmd_phase=1;this.cmd_flags=this.cmd_code=0;this.cmd_buffer=new Uint8Array(17);this.cmd_remaining=this.cmd_cursor=0;this.response_data=new Uint8Array(15);this.reset_sense_int_count=this.curr_drive_no=this.status1=this.status0=this.response_length=this.response_cursor=0;this.locked=!1;this.head_load_time=this.step_rate_interval=
0;this.fdc_config=96;this.eot=this.precomp_trk=0;this.drives=[new Rc("fda",d?.fda,b,4),new Rc("fdb",d?.fdb,c,4)];Object.seal(this);this.cpu.devices.rtc.cmos_write(16,this.drives[0].drive_type<<4|this.drives[1].drive_type);this.io.register_read(1008,this,this.read_reg_sra);this.io.register_read(1009,this,this.read_reg_srb);this.io.register_read(1010,this,this.read_reg_dor);this.io.register_read(1011,this,this.read_reg_tdr);this.io.register_read(1012,this,this.read_reg_msr);this.io.register_read(1013,
0;this.fdc_config=96;this.eot=this.precomp_trk=0;this.drives=[new Sc("fda",d?.fda,b,4),new Sc("fdb",d?.fdb,c,4)];Object.seal(this);this.cpu.devices.rtc.cmos_write(16,this.drives[0].drive_type<<4|this.drives[1].drive_type);this.io.register_read(1008,this,this.read_reg_sra);this.io.register_read(1009,this,this.read_reg_srb);this.io.register_read(1010,this,this.read_reg_dor);this.io.register_read(1011,this,this.read_reg_tdr);this.io.register_read(1012,this,this.read_reg_msr);this.io.register_read(1013,
this,this.read_reg_fifo);this.io.register_read(1015,this,this.read_reg_dir);this.io.register_write(1010,this,this.write_reg_dor);this.io.register_write(1011,this,this.write_reg_tdr);this.io.register_write(1012,this,this.write_reg_dsr);this.io.register_write(1013,this,this.write_reg_fifo);this.io.register_write(1015,this,this.write_reg_ccr)}
V.prototype.build_cmd_lookup_table=function(){const a=[{code:6,mask:31,argc:8,name:"READ",handler:this.exec_read},{code:5,mask:63,argc:8,name:"WRITE",handler:this.exec_write},{code:15,mask:255,argc:2,name:"SEEK",handler:this.exec_seek},{code:8,mask:255,argc:0,name:"SENSE INTERRUPT STATUS",handler:this.exec_sense_interrupt_status},{code:7,mask:255,argc:1,name:"RECALIBRATE",handler:this.exec_recalibrate},{code:13,mask:191,argc:5,name:"FORMAT TRACK",handler:this.exec_format_track},{code:2,mask:191,argc:8,
name:"READ TRACK",handler:this.exec_unimplemented},{code:78,mask:255,argc:17,name:"RESTORE",handler:this.exec_unimplemented},{code:46,mask:255,argc:0,name:"SAVE",handler:this.exec_unimplemented},{code:12,mask:31,argc:8,name:"READ DELETED DATA",handler:this.exec_unimplemented},{code:17,mask:31,argc:8,name:"SCAN EQUAL",handler:this.exec_unimplemented},{code:22,mask:31,argc:8,name:"VERIFY",handler:this.exec_unimplemented},{code:25,mask:31,argc:8,name:"SCAN LOW OR EQUAL",handler:this.exec_unimplemented},
@@ -459,32 +465,32 @@ V.prototype.get_state=function(){const a=[];a[19]=this.sra;a[20]=this.srb;a[21]=
a[41]=this.fdc_config;a[42]=this.precomp_trk;a[43]=this.eot;a[44]=this.drives[0].get_state();a[45]=this.drives[1].get_state();return a};
V.prototype.set_state=function(a){"undefined"!==typeof a[19]&&(this.sra=a[19],this.srb=a[20],this.dor=a[21],this.tdr=a[22],this.msr=a[23],this.dsr=a[24],this.cmd_phase=a[25],this.cmd_code=a[26],this.cmd_flags=a[27],this.cmd_buffer.set(a[28]),this.cmd_cursor=a[29],this.cmd_remaining=a[30],this.response_data.set(a[31]),this.response_cursor=a[32],this.response_length=a[33],this.status0=a[34],this.status1=a[35],this.curr_drive_no=a[36],this.reset_sense_int_count=a[37],this.locked=a[38],this.step_rate_interval=
a[39],this.head_load_time=a[40],this.fdc_config=a[41],this.precomp_trk=a[42],this.eot=a[43],this.drives[0].set_state(a[44]),this.drives[1].set_state(a[45]))};
const Sc=[{drive_type:4,sectors:18,tracks:80,heads:2},{drive_type:4,sectors:20,tracks:80,heads:2},{drive_type:4,sectors:21,tracks:80,heads:2},{drive_type:4,sectors:21,tracks:82,heads:2},{drive_type:4,sectors:21,tracks:83,heads:2},{drive_type:4,sectors:22,tracks:80,heads:2},{drive_type:4,sectors:23,tracks:80,heads:2},{drive_type:4,sectors:24,tracks:80,heads:2},{drive_type:5,sectors:36,tracks:80,heads:2},{drive_type:5,sectors:39,tracks:80,heads:2},{drive_type:5,sectors:40,tracks:80,heads:2},{drive_type:5,
const Tc=[{drive_type:4,sectors:18,tracks:80,heads:2},{drive_type:4,sectors:20,tracks:80,heads:2},{drive_type:4,sectors:21,tracks:80,heads:2},{drive_type:4,sectors:21,tracks:82,heads:2},{drive_type:4,sectors:21,tracks:83,heads:2},{drive_type:4,sectors:22,tracks:80,heads:2},{drive_type:4,sectors:23,tracks:80,heads:2},{drive_type:4,sectors:24,tracks:80,heads:2},{drive_type:5,sectors:36,tracks:80,heads:2},{drive_type:5,sectors:39,tracks:80,heads:2},{drive_type:5,sectors:40,tracks:80,heads:2},{drive_type:5,
sectors:44,tracks:80,heads:2},{drive_type:5,sectors:48,tracks:80,heads:2},{drive_type:4,sectors:8,tracks:80,heads:2},{drive_type:4,sectors:9,tracks:80,heads:2},{drive_type:4,sectors:10,tracks:80,heads:2},{drive_type:4,sectors:10,tracks:82,heads:2},{drive_type:4,sectors:10,tracks:83,heads:2},{drive_type:4,sectors:13,tracks:80,heads:2},{drive_type:4,sectors:14,tracks:80,heads:2},{drive_type:2,sectors:15,tracks:80,heads:2},{drive_type:2,sectors:18,tracks:80,heads:2},{drive_type:2,sectors:18,tracks:82,
heads:2},{drive_type:2,sectors:18,tracks:83,heads:2},{drive_type:2,sectors:20,tracks:80,heads:2},{drive_type:2,sectors:9,tracks:80,heads:2},{drive_type:2,sectors:11,tracks:80,heads:2},{drive_type:2,sectors:9,tracks:40,heads:2},{drive_type:2,sectors:9,tracks:40,heads:1},{drive_type:2,sectors:10,tracks:41,heads:2},{drive_type:2,sectors:10,tracks:42,heads:2},{drive_type:2,sectors:8,tracks:40,heads:2},{drive_type:2,sectors:8,tracks:40,heads:1},{drive_type:4,sectors:9,tracks:80,heads:1},{drive_type:1,
sectors:10,tracks:40,heads:1},{drive_type:1,sectors:10,tracks:40,heads:2}];function Rc(a,b,c,d){this.name=a;this.curr_head=this.curr_track=this.max_sect=this.max_head=this.max_track=this.drive_type=0;this.curr_sect=1;this.perpendicular=0;this.read_only=!1;this.media_changed=!0;this.buffer=null;Object.seal(this);a=b?.drive_type;void 0!==a&&0<=a&&5>=a&&(this.drive_type=a);this.insert_disk(c,!!b?.read_only);0===this.drive_type&&void 0===a&&(this.drive_type=d)}
Rc.prototype.insert_disk=function(a,b){if(!a)return!1;a instanceof Uint8Array&&(a=new z(a.buffer));const [c,d]=this.find_disk_format(a,this.drive_type);if(!c)return!1;this.max_track=d.tracks;this.max_head=d.heads;this.max_sect=d.sectors;this.read_only=!!b;this.media_changed=!0;this.buffer=c;0===this.drive_type&&(this.drive_type=d.drive_type);return!0};Rc.prototype.eject_disk=function(){this.max_sect=this.max_head=this.max_track=0;this.media_changed=!0;this.buffer=null};
Rc.prototype.get_buffer=function(){return this.buffer?new Uint8Array(this.buffer.buffer):null};Rc.prototype.chs2lba=function(a,b,c){return(a*this.max_head+b)*this.max_sect+c-1};
Rc.prototype.find_disk_format=function(a,b){const c=0===b,d=a.byteLength;let e=-1,f=-1,g=-1,h=-1,l=-1;for(let m=0;m<Sc.length;m++){const n=Sc[m],p=n.sectors*n.tracks*n.heads*512;if(d===p)if(c||n.drive_type===b){e=m;break}else c||Qc[n.drive_type]!==Qc[b]?g=-1===g?m:g:f=-1===f?m:f;else d<p&&(-1===l||p<l)&&(h=m,l=p)}return-1!==e?[a,Sc[e]]:-1!==f?[a,Sc[f]]:-1!==g?[a,Sc[g]]:-1!==h?(b=new Uint8Array(l),b.set(new Uint8Array(a.buffer)),[new z(b.buffer),Sc[h]]):[null,null]};
Rc.prototype.seek=function(a,b,c){if(b>this.max_track||0!==a&&1===this.max_head)return 2;if(c>this.max_sect)return 3;let d=0;const e=this.chs2lba(this.curr_track,this.curr_head,this.curr_sect),f=this.chs2lba(b,a,c);e!==f&&(this.curr_track!==b&&(this.buffer&&(this.media_changed=!1),d=1),this.curr_head=a,this.curr_track=b,this.curr_sect=c);this.buffer||(d=2);return d};
Rc.prototype.get_state=function(){const a=[];a[0]=this.drive_type;a[1]=this.max_track;a[2]=this.max_head;a[3]=this.max_sect;a[4]=this.curr_track;a[5]=this.curr_head;a[6]=this.curr_sect;a[7]=this.perpendicular;a[8]=this.read_only;a[9]=this.media_changed;a[10]=this.buffer?this.buffer.get_state():null;return a};
Rc.prototype.set_state=function(a){this.drive_type=a[0];this.max_track=a[1];this.max_head=a[2];this.max_sect=a[3];this.curr_track=a[4];this.curr_head=a[5];this.curr_sect=a[6];this.perpendicular=a[7];this.read_only=a[8];this.media_changed=a[9];a[10]?(this.buffer||(this.buffer=new z(new ArrayBuffer(0))),this.buffer.set_state(a[10])):this.buffer=null};const Tc={[70]:{name:"GET CONFIGURATION",flags:0},[74]:{name:"GET EVENT STATUS NOTIFICATION",flags:0},[18]:{name:"INQUIRY",flags:0},[189]:{name:"MECHANISM STATUS",flags:0},[26]:{name:"MODE SENSE (6)",flags:0},[90]:{name:"MODE SENSE (10)",flags:0},[69]:{name:"PAUSE",flags:1},[30]:{name:"PREVENT ALLOW MEDIUM REMOVAL",flags:0},[40]:{name:"READ (10)",flags:1},[168]:{name:"READ (12)",flags:1},[37]:{name:"READ CAPACITY",flags:1},[190]:{name:"READ CD",flags:1},[81]:{name:"READ DISK INFORMATION",flags:1},
sectors:10,tracks:40,heads:1},{drive_type:1,sectors:10,tracks:40,heads:2}];function Sc(a,b,c,d){this.name=a;this.curr_head=this.curr_track=this.max_sect=this.max_head=this.max_track=this.drive_type=0;this.curr_sect=1;this.perpendicular=0;this.read_only=!1;this.media_changed=!0;this.buffer=null;Object.seal(this);a=b?.drive_type;void 0!==a&&0<=a&&5>=a&&(this.drive_type=a);this.insert_disk(c,!!b?.read_only);0===this.drive_type&&void 0===a&&(this.drive_type=d)}
Sc.prototype.insert_disk=function(a,b){if(!a)return!1;a instanceof Uint8Array&&(a=new z(a.buffer));const [c,d]=this.find_disk_format(a,this.drive_type);if(!c)return!1;this.max_track=d.tracks;this.max_head=d.heads;this.max_sect=d.sectors;this.read_only=!!b;this.media_changed=!0;this.buffer=c;0===this.drive_type&&(this.drive_type=d.drive_type);return!0};Sc.prototype.eject_disk=function(){this.max_sect=this.max_head=this.max_track=0;this.media_changed=!0;this.buffer=null};
Sc.prototype.get_buffer=function(){return this.buffer?new Uint8Array(this.buffer.buffer):null};Sc.prototype.chs2lba=function(a,b,c){return(a*this.max_head+b)*this.max_sect+c-1};
Sc.prototype.find_disk_format=function(a,b){const c=0===b,d=a.byteLength;let e=-1,f=-1,g=-1,h=-1,l=-1;for(let m=0;m<Tc.length;m++){const n=Tc[m],p=n.sectors*n.tracks*n.heads*512;if(d===p)if(c||n.drive_type===b){e=m;break}else c||Rc[n.drive_type]!==Rc[b]?g=-1===g?m:g:f=-1===f?m:f;else d<p&&(-1===l||p<l)&&(h=m,l=p)}return-1!==e?[a,Tc[e]]:-1!==f?[a,Tc[f]]:-1!==g?[a,Tc[g]]:-1!==h?(b=new Uint8Array(l),b.set(new Uint8Array(a.buffer)),[new z(b.buffer),Tc[h]]):[null,null]};
Sc.prototype.seek=function(a,b,c){if(b>this.max_track||0!==a&&1===this.max_head)return 2;if(c>this.max_sect)return 3;let d=0;const e=this.chs2lba(this.curr_track,this.curr_head,this.curr_sect),f=this.chs2lba(b,a,c);e!==f&&(this.curr_track!==b&&(this.buffer&&(this.media_changed=!1),d=1),this.curr_head=a,this.curr_track=b,this.curr_sect=c);this.buffer||(d=2);return d};
Sc.prototype.get_state=function(){const a=[];a[0]=this.drive_type;a[1]=this.max_track;a[2]=this.max_head;a[3]=this.max_sect;a[4]=this.curr_track;a[5]=this.curr_head;a[6]=this.curr_sect;a[7]=this.perpendicular;a[8]=this.read_only;a[9]=this.media_changed;a[10]=this.buffer?this.buffer.get_state():null;return a};
Sc.prototype.set_state=function(a){this.drive_type=a[0];this.max_track=a[1];this.max_head=a[2];this.max_sect=a[3];this.curr_track=a[4];this.curr_head=a[5];this.curr_sect=a[6];this.perpendicular=a[7];this.read_only=a[8];this.media_changed=a[9];a[10]?(this.buffer||(this.buffer=new z(new ArrayBuffer(0))),this.buffer.set_state(a[10])):this.buffer=null};const Uc={[70]:{name:"GET CONFIGURATION",flags:0},[74]:{name:"GET EVENT STATUS NOTIFICATION",flags:0},[18]:{name:"INQUIRY",flags:0},[189]:{name:"MECHANISM STATUS",flags:0},[26]:{name:"MODE SENSE (6)",flags:0},[90]:{name:"MODE SENSE (10)",flags:0},[69]:{name:"PAUSE",flags:1},[30]:{name:"PREVENT ALLOW MEDIUM REMOVAL",flags:0},[40]:{name:"READ (10)",flags:1},[168]:{name:"READ (12)",flags:1},[37]:{name:"READ CAPACITY",flags:1},[190]:{name:"READ CD",flags:1},[81]:{name:"READ DISK INFORMATION",flags:1},
[66]:{name:"READ SUBCHANNEL",flags:1},[67]:{name:"READ TOC PMA ATIP",flags:1},[82]:{name:"READ TRACK INFORMATION",flags:1},[3]:{name:"REQUEST SENSE",flags:0},[27]:{name:"START STOP UNIT",flags:0},[0]:{name:"TEST UNIT READY",flags:1}};
function Uc(a,b,c){this.cpu=a;this.bus=b;this.secondary=this.primary=void 0;b=c&&c[0][0];const d=c&&c[1][0];if(b||d){b&&(this.primary=new Vc(this,0,c[0],496,1014,14));d&&(this.secondary=new Vc(this,1,c[1],368,886,15));c=b?this.primary.command_base:0;const e=b?this.primary.control_base:0,f=d?this.secondary.command_base:0,g=d?this.secondary.control_base:0;this.name="ide";this.pci_id=240;this.pci_space=[134,128,16,112,5,0,160,2,0,128,1,1,0,0,0,0,c&255|1,c>>8,0,0,e&255|1,e>>8,0,0,f&255|1,f>>8,0,0,g&255|
function Vc(a,b,c){this.cpu=a;this.bus=b;this.secondary=this.primary=void 0;b=c&&c[0][0];const d=c&&c[1][0];if(b||d){b&&(this.primary=new Wc(this,0,c[0],496,1014,14));d&&(this.secondary=new Wc(this,1,c[1],368,886,15));c=b?this.primary.command_base:0;const e=b?this.primary.control_base:0,f=d?this.secondary.command_base:0,g=d?this.secondary.control_base:0;this.name="ide";this.pci_id=240;this.pci_space=[134,128,16,112,5,0,160,2,0,128,1,1,0,0,0,0,c&255|1,c>>8,0,0,e&255|1,e>>8,0,0,f&255|1,f>>8,0,0,g&255|
1,g>>8,0,0,1,180,0,0,0,0,0,0,0,0,0,0,67,16,212,130,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];this.pci_bars=[b?{size:8}:void 0,b?{size:1}:void 0,d?{size:8}:void 0,d?{size:1}:void 0,{size:16}];a.devices.pci.register_device(this)}Object.seal(this)}
Uc.prototype.get_state=function(){const a=[];a[0]=this.primary;a[1]=this.secondary;return a};Uc.prototype.set_state=function(a){this.primary&&this.primary.set_state(a[0]);this.secondary&&this.secondary.set_state(a[1])};
function Vc(a,b,c,d,e,f){this.controller=a;this.channel_nr=b;this.cpu=a.cpu;this.bus=a.bus;this.command_base=d;this.control_base=e;this.irq=f;this.name="ide"+b;d=c?c[0]:void 0;c=c?c[1]:void 0;this.master=new W(this,0,d?.buffer,d?.is_cdrom);this.slave=new W(this,1,c?.buffer,c?.is_cdrom);this.current_interface=this.master;this.device_control_reg=2;this.dma_command=this.dma_status=this.prdt_addr=0;a=a.cpu;a.io.register_read(this.command_base|0,this,function(){return this.current_interface.read_data(1)},
Vc.prototype.get_state=function(){const a=[];a[0]=this.primary;a[1]=this.secondary;return a};Vc.prototype.set_state=function(a){this.primary&&this.primary.set_state(a[0]);this.secondary&&this.secondary.set_state(a[1])};
function Wc(a,b,c,d,e,f){this.controller=a;this.channel_nr=b;this.cpu=a.cpu;this.bus=a.bus;this.command_base=d;this.control_base=e;this.irq=f;this.name="ide"+b;d=c?c[0]:void 0;c=c?c[1]:void 0;this.master=new W(this,0,d?.buffer,d?.is_cdrom);this.slave=new W(this,1,c?.buffer,c?.is_cdrom);this.current_interface=this.master;this.device_control_reg=2;this.dma_command=this.dma_status=this.prdt_addr=0;a=a.cpu;a.io.register_read(this.command_base|0,this,function(){return this.current_interface.read_data(1)},
function(){return this.current_interface.read_data(2)},function(){return this.current_interface.read_data(4)});a.io.register_read(this.command_base|1,this,function(){return this.current_interface.error_reg&255});a.io.register_read(this.command_base|2,this,function(){return this.current_interface.sector_count_reg&255});a.io.register_read(this.command_base|3,this,function(){return this.current_interface.lba_low_reg&255});a.io.register_read(this.command_base|4,this,function(){return this.current_interface.lba_mid_reg&
255});a.io.register_read(this.command_base|5,this,function(){return this.current_interface.lba_high_reg&255});a.io.register_read(this.command_base|6,this,function(){return this.current_interface.device_reg&255});a.io.register_read(this.command_base|7,this,function(){const g=this.read_status();this.cpu.device_lower_irq(this.irq);return g});a.io.register_write(this.command_base|0,this,function(g){this.current_interface.write_data_port8(g)},function(g){this.current_interface.write_data_port16(g)},function(g){this.current_interface.write_data_port32(g)});
a.io.register_write(this.command_base|1,this,function(g){this.master.features_reg=(this.master.features_reg<<8|g)&65535;this.slave.features_reg=(this.slave.features_reg<<8|g)&65535});a.io.register_write(this.command_base|2,this,function(g){this.master.sector_count_reg=(this.master.sector_count_reg<<8|g)&65535;this.slave.sector_count_reg=(this.slave.sector_count_reg<<8|g)&65535});a.io.register_write(this.command_base|3,this,function(g){this.master.lba_low_reg=(this.master.lba_low_reg<<8|g)&65535;this.slave.lba_low_reg=
(this.slave.lba_low_reg<<8|g)&65535});a.io.register_write(this.command_base|4,this,function(g){this.master.lba_mid_reg=(this.master.lba_mid_reg<<8|g)&65535;this.slave.lba_mid_reg=(this.slave.lba_mid_reg<<8|g)&65535});a.io.register_write(this.command_base|5,this,function(g){this.master.lba_high_reg=(this.master.lba_high_reg<<8|g)&65535;this.slave.lba_high_reg=(this.slave.lba_high_reg<<8|g)&65535});a.io.register_write(this.command_base|6,this,function(g){const h=g&16;if(h&&this.current_interface===
this.master||!h&&this.current_interface===this.slave)this.current_interface=h?this.slave:this.master;this.current_interface.device_reg=g;this.current_interface.is_lba=g>>6&1;this.current_interface.head=g&15});a.io.register_write(this.command_base|7,this,function(g){this.current_interface.status_reg&=-34;this.current_interface.ata_command(g);this.cpu.device_lower_irq(this.irq)});a.io.register_read(this.control_base|0,this,this.read_status);a.io.register_write(this.control_base|0,this,this.write_control);
b=46080+8*b;a.io.register_read(b|0,this,this.dma_read_command8,void 0,this.dma_read_command);a.io.register_write(b|0,this,this.dma_write_command8,void 0,this.dma_write_command);a.io.register_read(b|2,this,this.dma_read_status);a.io.register_write(b|2,this,this.dma_write_status);a.io.register_read(b|4,this,void 0,void 0,this.dma_read_addr);a.io.register_write(b|4,this,void 0,void 0,this.dma_set_addr)}
Vc.prototype.read_status=function(){return this.current_interface.drive_connected?this.current_interface.status_reg:0};Vc.prototype.write_control=function(a){a&4&&(this.cpu.device_lower_irq(this.irq),this.master.device_reset(),this.slave.device_reset());this.device_control_reg=a};Vc.prototype.dma_read_addr=function(){return this.prdt_addr};Vc.prototype.dma_set_addr=function(a){this.prdt_addr=a};Vc.prototype.dma_read_status=function(){return this.dma_status};
Vc.prototype.dma_write_status=function(a){this.dma_status&=~(a&6)};Vc.prototype.dma_read_command=function(){return this.dma_read_command8()|this.dma_read_status()<<16};Vc.prototype.dma_read_command8=function(){return this.dma_command};Vc.prototype.dma_write_command=function(a){this.dma_write_command8(a&255);this.dma_write_status(a>>16&255)};
Vc.prototype.dma_write_command8=function(a){const b=this.dma_command;this.dma_command=a&9;if((b&1)!==(a&1))if(0===(a&1))this.dma_status&=-2;else switch(this.dma_status|=1,this.current_interface.current_command){case 200:case 37:this.current_interface.do_ata_read_sectors_dma();break;case 202:case 53:this.current_interface.do_ata_write_sectors_dma();break;case 160:this.current_interface.do_atapi_dma();break;default:y(this.current_interface.current_command),this.dma_status&=-2,this.dma_status|=2,this.push_irq()}};
Vc.prototype.push_irq=function(){0===(this.device_control_reg&2)&&(this.dma_status|=4,this.cpu.device_raise_irq(this.irq))};Vc.prototype.get_state=function(){var a=[];a[0]=this.master;a[1]=this.slave;a[2]=this.command_base;a[3]=this.irq;a[5]=this.control_base;a[7]=this.name;a[8]=this.device_control_reg;a[9]=this.prdt_addr;a[10]=this.dma_status;a[11]=this.current_interface===this.master;a[12]=this.dma_command;return a};
Vc.prototype.set_state=function(a){this.master.set_state(a[0]);this.slave.set_state(a[1]);this.command_base=a[2];this.irq=a[3];this.control_base=a[5];this.name=a[7];this.device_control_reg=a[8];this.prdt_addr=a[9];this.dma_status=a[10];this.current_interface=a[11]?this.master:this.slave;this.dma_command=a[12]};
Wc.prototype.read_status=function(){return this.current_interface.drive_connected?this.current_interface.status_reg:0};Wc.prototype.write_control=function(a){a&4&&(this.cpu.device_lower_irq(this.irq),this.master.device_reset(),this.slave.device_reset());this.device_control_reg=a};Wc.prototype.dma_read_addr=function(){return this.prdt_addr};Wc.prototype.dma_set_addr=function(a){this.prdt_addr=a};Wc.prototype.dma_read_status=function(){return this.dma_status};
Wc.prototype.dma_write_status=function(a){this.dma_status&=~(a&6)};Wc.prototype.dma_read_command=function(){return this.dma_read_command8()|this.dma_read_status()<<16};Wc.prototype.dma_read_command8=function(){return this.dma_command};Wc.prototype.dma_write_command=function(a){this.dma_write_command8(a&255);this.dma_write_status(a>>16&255)};
Wc.prototype.dma_write_command8=function(a){const b=this.dma_command;this.dma_command=a&9;if((b&1)!==(a&1))if(0===(a&1))this.dma_status&=-2;else switch(this.dma_status|=1,this.current_interface.current_command){case 200:case 37:this.current_interface.do_ata_read_sectors_dma();break;case 202:case 53:this.current_interface.do_ata_write_sectors_dma();break;case 160:this.current_interface.do_atapi_dma();break;default:y(this.current_interface.current_command),this.dma_status&=-2,this.dma_status|=2,this.push_irq()}};
Wc.prototype.push_irq=function(){0===(this.device_control_reg&2)&&(this.dma_status|=4,this.cpu.device_raise_irq(this.irq))};Wc.prototype.get_state=function(){var a=[];a[0]=this.master;a[1]=this.slave;a[2]=this.command_base;a[3]=this.irq;a[5]=this.control_base;a[7]=this.name;a[8]=this.device_control_reg;a[9]=this.prdt_addr;a[10]=this.dma_status;a[11]=this.current_interface===this.master;a[12]=this.dma_command;return a};
Wc.prototype.set_state=function(a){this.master.set_state(a[0]);this.slave.set_state(a[1]);this.command_base=a[2];this.irq=a[3];this.control_base=a[5];this.name=a[7];this.device_control_reg=a[8];this.prdt_addr=a[9];this.dma_status=a[10];this.current_interface=a[11]?this.master:this.slave;this.dma_command=a[12]};
function W(a,b,c,d){this.channel=a;this.name=a.name+"."+b;this.bus=a.bus;this.channel_nr=a.channel_nr;this.interface_nr=b;this.cpu=a.cpu;this.buffer=null;this.drive_connected=d||!!c;this.sector_size=d?2048:512;this.is_atapi=d;this.sector_count=0;this.head_count=this.is_atapi?1:0;this.device_reg=this.head=this.lba_high_reg=this.lba_mid_reg=this.features_reg=this.lba_low_reg=this.sector_count_reg=this.is_lba=this.cylinder_count=this.sectors_per_track=0;this.status_reg=80;this.sectors_per_drq=128;this.data_pointer=
this.error_reg=0;this.data=new Uint8Array(65536);this.data16=new Uint16Array(this.data.buffer);this.data32=new Int32Array(this.data.buffer);this.data_end=this.data_length=0;this.current_command=-1;this.last_io_id=this.write_dest=0;this.in_progress_io_ids=new Set;this.cancelled_io_ids=new Set;this.current_atapi_command=-1;this.atapi_add_sense=this.atapi_sense_key=0;this.medium_changed=!1;this.set_disk_buffer(c);Object.seal(this)}W.prototype.has_disk=function(){return!!this.buffer};
W.prototype.eject=function(){this.is_atapi&&this.buffer&&(this.medium_changed=!0,this.buffer=null,this.status_reg=89,this.error_reg=96,this.push_irq())};W.prototype.set_cdrom=function(a){this.is_atapi&&a&&(this.set_disk_buffer(a),this.medium_changed=!0)};
@@ -498,7 +504,7 @@ W.prototype.ata_command=function(a){if(this.drive_connected||144===a)switch(this
break;case 198:y(this.sector_count_reg&255);this.sectors_per_drq=this.sector_count_reg&255;this.status_reg=80;this.push_irq();break;case 200:case 37:this.ata_read_sectors_dma(a);break;case 202:case 53:this.ata_write_sectors_dma(a);break;case 64:this.status_reg=80;this.push_irq();break;case 218:this.is_atapi&&(this.buffer||(this.error_reg|=2),this.medium_changed&&(this.error_reg|=32,this.medium_changed=!1),this.error_reg|=64);this.status_reg=80;this.push_irq();break;case 224:this.status_reg=80;this.push_irq();
break;case 225:this.status_reg=80;this.push_irq();break;case 231:this.status_reg=80;this.push_irq();break;case 234:this.status_reg=80;this.push_irq();break;case 236:this.is_atapi?(this.lba_mid_reg=20,this.lba_high_reg=235,this.ata_abort_command()):(this.create_identify_packet(),this.status_reg=88,this.push_irq());break;case 239:this.status_reg=80;this.push_irq();break;case 222:this.status_reg=64;this.push_irq();break;case 245:this.status_reg=80;this.push_irq();break;case 249:this.ata_abort_command();
break;case 0:this.ata_abort_command();break;case 240:y(a);this.capture_regs();this.ata_abort_command();break;default:y(a),this.capture_regs(),this.ata_abort_command()}else y(a)};
W.prototype.atapi_handle=function(){var a=this.data[0],b=Tc[a]?Tc[a].flags:0;this.data_pointer=0;this.current_atapi_command=a;3!==a&&(this.atapi_add_sense=this.atapi_sense_key=0);if(!this.buffer&&b&1)this.atapi_check_condition_response(2,58),this.push_irq();else{switch(a){case 0:this.buffer?(this.data_allocate(0),this.data_end=this.data_length,this.status_reg=80):this.atapi_check_condition_response(2,58);break;case 3:this.data_allocate(this.data[4]);this.data_end=this.data_length;this.status_reg=
W.prototype.atapi_handle=function(){var a=this.data[0],b=Uc[a]?Uc[a].flags:0;this.data_pointer=0;this.current_atapi_command=a;3!==a&&(this.atapi_add_sense=this.atapi_sense_key=0);if(!this.buffer&&b&1)this.atapi_check_condition_response(2,58),this.push_irq();else{switch(a){case 0:this.buffer?(this.data_allocate(0),this.data_end=this.data_length,this.status_reg=80):this.atapi_check_condition_response(2,58);break;case 3:this.data_allocate(this.data[4]);this.data_end=this.data_length;this.status_reg=
88;this.data[0]=240;this.data[2]=this.atapi_sense_key;this.data[7]=8;this.data[12]=this.atapi_add_sense;this.atapi_add_sense=this.atapi_sense_key=0;break;case 18:a=this.data[4];this.status_reg=88;y(this.data[1],2);this.data.set([5,128,1,49,31,0,0,0,83,79,78,89,32,32,32,32,67,68,45,82,79,77,32,67,68,85,45,49,48,48,48,32,49,46,49,97]);this.data_end=this.data_length=Math.min(36,a);break;case 26:this.data_allocate(this.data[4]);this.data_end=this.data_length;this.status_reg=88;break;case 30:this.data_allocate(0);
this.data_end=this.data_length;this.status_reg=80;break;case 37:a=this.sector_count-1;this.data_set(new Uint8Array([a>>24&255,a>>16&255,a>>8&255,a&255,0,0,this.sector_size>>8&255,this.sector_size&255]));this.data_end=this.data_length;this.status_reg=88;break;case 40:case 168:this.features_reg&1?this.atapi_read_dma(this.data):this.atapi_read(this.data);break;case 66:a=this.data[8];this.data_allocate(Math.min(8,a));this.data_end=this.data_length;this.status_reg=88;break;case 67:a=this.data[8]|this.data[7]<<
8;b=this.data[9]>>6;y(b,2);y(this.data[6]);this.data_allocate(a);this.data_end=this.data_length;0===b?(a=this.sector_count,this.data.set(new Uint8Array([0,18,1,1,0,20,1,0,0,0,0,0,0,22,170,0,a>>24,a>>16&255,a>>8&255,a&255]))):1===b&&this.data.set(new Uint8Array([0,10,1,1,0,0,0,0,0,0,0,0]));this.status_reg=88;break;case 70:a=Math.min(this.data[8]|this.data[7]<<8,32);this.data_allocate(a);this.data_end=this.data_length;this.data[0]=a-4>>24&255;this.data[1]=a-4>>16&255;this.data[2]=a-4>>8&255;this.data[3]=
@@ -535,12 +541,12 @@ W.prototype.cancel_io_operations=function(){for(const a of this.in_progress_io_i
W.prototype.get_state=function(){var a=[];a[0]=this.sector_count_reg;a[1]=this.cylinder_count;a[2]=this.lba_high_reg;a[3]=this.lba_mid_reg;a[4]=this.data_pointer;a[5]=0;a[6]=0;a[7]=0;a[8]=0;a[9]=this.device_reg;a[10]=this.error_reg;a[11]=this.head;a[12]=this.head_count;a[13]=this.is_atapi;a[14]=this.is_lba;a[15]=this.features_reg;a[16]=this.data;a[17]=this.data_length;a[18]=this.lba_low_reg;a[19]=this.sector_count;a[20]=this.sector_size;a[21]=this.sectors_per_drq;a[22]=this.sectors_per_track;a[23]=
this.status_reg;a[24]=this.write_dest;a[25]=this.current_command;a[26]=this.data_end;a[27]=this.current_atapi_command;a[28]=this.buffer;return a};
W.prototype.set_state=function(a){this.sector_count_reg=a[0];this.cylinder_count=a[1];this.lba_high_reg=a[2];this.lba_mid_reg=a[3];this.data_pointer=a[4];this.device_reg=a[9];this.error_reg=a[10];this.head=a[11];this.head_count=a[12];this.is_atapi=a[13];this.is_lba=a[14];this.features_reg=a[15];this.data=a[16];this.data_length=a[17];this.lba_low_reg=a[18];this.sector_count=a[19];this.sector_size=a[20];this.sectors_per_drq=a[21];this.sectors_per_track=a[22];this.status_reg=a[23];this.write_dest=a[24];
this.current_command=a[25];this.data_end=a[26];this.current_atapi_command=a[27];this.data16=new Uint16Array(this.data.buffer);this.data32=new Int32Array(this.data.buffer);this.buffer&&this.buffer.set_state(a[28]);this.drive_connected=this.is_atapi||this.buffer;this.medium_changed=!1};function Wc(a,b,c,d=1500){this.bus=b;this.id=a.devices.net?1:0;this.status=this.pairs=1;this.preserve_mac_from_state_image=c;this.mac=new Uint8Array([0,34,21,255*Math.random()|0,255*Math.random()|0,255*Math.random()|0]);this.bus.send("net"+this.id+"-mac",Cc(this.mac));b=[];for(c=0;c<this.pairs;++c)b.push({size_supported:1024,notify_offset:0}),b.push({size_supported:1024,notify_offset:1});b.push({size_supported:16,notify_offset:2});this.virtio=new Fc(a,{name:"virtio-net",pci_id:80,device_id:4161,subsystem_device_id:1,
this.current_command=a[25];this.data_end=a[26];this.current_atapi_command=a[27];this.data16=new Uint16Array(this.data.buffer);this.data32=new Int32Array(this.data.buffer);this.buffer&&this.buffer.set_state(a[28]);this.drive_connected=this.is_atapi||this.buffer;this.medium_changed=!1};function Xc(a,b,c,d=1500){this.bus=b;this.id=a.devices.net?1:0;this.status=this.pairs=1;this.preserve_mac_from_state_image=c;this.mac=new Uint8Array([0,34,21,255*Math.random()|0,255*Math.random()|0,255*Math.random()|0]);this.bus.send("net"+this.id+"-mac",Cc(this.mac));b=[];for(c=0;c<this.pairs;++c)b.push({size_supported:1024,notify_offset:0}),b.push({size_supported:1024,notify_offset:1});b.push({size_supported:16,notify_offset:2});this.virtio=new Fc(a,{name:"virtio-net",pci_id:80,device_id:4161,subsystem_device_id:1,
common:{initial_port:51200,queues:b,features:[5,16,22,3,17,23,32],on_driver_ok:()=>{}},notification:{initial_port:51456,single_handler:!1,handlers:[()=>{},e=>{const f=this.virtio.queues[e];for(;f.has_request();){const g=f.pop_request(),h=new Uint8Array(g.length_readable);g.get_next_blob(h);this.bus.send("net"+this.id+"-send",h.subarray(12));this.bus.send("eth-transmit-end",[h.length-12]);this.virtio.queues[e].push_reply(g)}this.virtio.queues[e].flush_replies()},e=>{if(e===2*this.pairs)for(var f=this.virtio.queues[e];f.has_request();){const g=
f.pop_request(),h=new Uint8Array(g.length_readable);g.get_next_blob(h);const l=I(["b","b"],h,{offset:0});switch(l[0]<<8|l[1]){case 1024:I(["h"],h,{offset:2});this.Send(e,g,new Uint8Array([0]));break;case 257:this.mac=h.subarray(2,8);this.Send(e,g,new Uint8Array([0]));this.bus.send("net"+this.id+"-mac",Cc(this.mac));break;default:this.Send(e,g,new Uint8Array([1]));return}}}]},isr_status:{initial_port:50944},device_specific:{initial_port:50688,struct:[0,1,2,3,4,5].map((e,f)=>({bytes:1,name:"mac_"+f,
read:()=>this.mac[f],write:()=>{}})).concat([{bytes:2,name:"status",read:()=>this.status,write:()=>{}},{bytes:2,name:"max_pairs",read:()=>this.pairs,write:()=>{}},{bytes:2,name:"mtu",read:()=>d,write:()=>{}}])}});this.bus.register("net"+this.id+"-receive",e=>{this.bus.send("eth-receive-end",[e.length]);const f=new Uint8Array(12+e.byteLength);(new DataView(f.buffer,f.byteOffset,f.byteLength)).setInt16(10,1);f.set(e,12);e=this.virtio.queues[0];e.has_request()?(e=e.pop_request(),e.set_next_blob(f),this.virtio.queues[0].push_reply(e),
this.virtio.queues[0].flush_replies()):console.log("No buffer to write into!")},this)}Wc.prototype.get_state=function(){const a=[];a[0]=this.virtio;a[1]=this.id;a[2]=this.mac;return a};Wc.prototype.set_state=function(a){this.virtio.set_state(a[0]);this.id=a[1];this.preserve_mac_from_state_image&&(this.mac=a[2],this.bus.send("net"+this.id+"-mac",Cc(this.mac)))};Wc.prototype.reset=function(){this.virtio.reset()};
Wc.prototype.Send=function(a,b,c){b.set_next_blob(c);this.virtio.queues[a].push_reply(b);this.virtio.queues[a].flush_replies()};Wc.prototype.Ack=function(a,b){this.virtio.queues[a].push_reply(b);this.virtio.queues[a].flush_replies()};const Xc=Uint32Array.from([655360,655360,720896,753664]),Yc=Uint32Array.from([131072,65536,32768,32768]);
this.virtio.queues[0].flush_replies()):console.log("No buffer to write into!")},this)}Xc.prototype.get_state=function(){const a=[];a[0]=this.virtio;a[1]=this.id;a[2]=this.mac;return a};Xc.prototype.set_state=function(a){this.virtio.set_state(a[0]);this.id=a[1];this.preserve_mac_from_state_image&&(this.mac=a[2],this.bus.send("net"+this.id+"-mac",Cc(this.mac)))};Xc.prototype.reset=function(){this.virtio.reset()};
Xc.prototype.Send=function(a,b,c){b.set_next_blob(c);this.virtio.queues[a].push_reply(b);this.virtio.queues[a].flush_replies()};Xc.prototype.Ack=function(a,b){this.virtio.queues[a].push_reply(b);this.virtio.queues[a].flush_replies()};const Yc=Uint32Array.from([655360,655360,720896,753664]),Zc=Uint32Array.from([131072,65536,32768,32768]);
function X(a,b,c,d){this.cpu=a;this.bus=b;this.screen=c;this.vga_memory_size=d;this.cursor_address=0;this.cursor_scanline_start=14;this.cursor_scanline_end=15;this.max_cols=80;this.max_rows=25;this.virtual_height=this.virtual_width=this.screen_height=this.screen_width=0;this.layers=[];this.start_address_latched=this.start_address=0;this.crtc=new Uint8Array(25);this.line_compare=this.offset_register=this.preset_row_scan=this.underline_location_register=this.vertical_blank_start=this.vertical_display_enable_end=
this.horizontal_blank_start=this.horizontal_display_enable_end=this.crtc_mode=0;this.graphical_mode=!1;this.vga256_palette=new Int32Array(256);this.latch_dword=0;this.svga_version=45253;this.svga_height=this.svga_width=0;this.svga_enabled=!1;this.svga_bpp=32;this.svga_offset_y=this.svga_offset_x=this.svga_offset=this.svga_bank_offset=0;this.vga_memory_size=void 0===this.vga_memory_size||262144>this.vga_memory_size?262144:268435456<this.vga_memory_size?268435456:ia(this.vga_memory_size);this.pci_space=
[52,18,17,17,3,1,0,0,0,0,0,3,0,0,0,0,8,14680064,57344,224,0,0,0,0,0,0,191,254,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,244,26,0,17,0,0,190,254,0,0,0,0,0,0,0,0,0,0,0,0];this.pci_id=144;this.pci_bars=[{size:this.vga_memory_size}];this.pci_rom_size=65536;this.pci_rom_address=4272947200;this.name="vga";this.dac_state=this.dac_color_index_read=this.dac_color_index_write=this.index_crtc=0;this.dac_mask=255;this.dac_map=new Uint8Array(16);this.attribute_controller_index=-1;this.palette_source=32;this.color_select=
@@ -559,9 +565,9 @@ a[18];this.svga_bpp=a[19];this.svga_bank_offset=a[20];this.svga_offset=a[21];thi
a[37];this.dispi_enable_value=a[38];this.svga_memory.set(a[39]);this.attribute_controller_index=a[41];this.offset_register=a[42];this.planar_setreset=a[43];this.planar_setreset_enable=a[44];this.start_address_latched=a[45];this.crtc.set(a[46]);this.horizontal_display_enable_end=a[47];this.horizontal_blank_start=a[48];this.vertical_display_enable_end=a[49];this.vertical_blank_start=a[50];this.underline_location_register=a[51];this.preset_row_scan=a[52];this.offset_register=a[53];this.palette_source=
a[54];this.attribute_mode=a[55];this.color_plane_enable=a[56];this.horizontal_panning=a[57];this.color_select=a[58];this.clocking_mode=a[59];this.line_compare=a[60];a[61]&&this.pixel_buffer.set(a[61]);this.dac_mask=void 0===a[62]?255:a[62];this.character_map_select=void 0===a[63]?0:a[63];this.font_page_ab_enabled=void 0===a[64]?0:a[64];this.screen.set_mode(this.graphical_mode);this.graphical_mode?(this.screen_height=this.screen_width=0,this.svga_enabled?(this.set_size_graphical(this.svga_width,this.svga_height,
this.svga_width,this.svga_height,this.svga_bpp),this.update_layers()):(this.update_vga_size(),this.update_layers(),this.complete_replot())):(this.set_font_bitmap(!0),this.set_size_text(this.max_cols,this.max_rows),this.set_font_page(),this.update_cursor_scanline(),this.update_cursor());this.complete_redraw()};
X.prototype.vga_memory_read=function(a){if(this.svga_enabled)return this.cpu.read8((a-655360|this.svga_bank_offset)+3758096384|0);var b=this.miscellaneous_graphics_register>>2&3;a-=Xc[b];if(0>a||a>=Yc[b])return y(a>>>0),0;this.latch_dword=this.plane0[a];this.latch_dword|=this.plane1[a]<<8;this.latch_dword|=this.plane2[a]<<16;this.latch_dword|=this.plane3[a]<<24;if(this.planar_mode&8)return b=255,this.color_dont_care&1&&(b&=this.plane0[a]^~(this.color_compare&1?255:0)),this.color_dont_care&2&&(b&=
X.prototype.vga_memory_read=function(a){if(this.svga_enabled)return this.cpu.read8((a-655360|this.svga_bank_offset)+3758096384|0);var b=this.miscellaneous_graphics_register>>2&3;a-=Yc[b];if(0>a||a>=Zc[b])return y(a>>>0),0;this.latch_dword=this.plane0[a];this.latch_dword|=this.plane1[a]<<8;this.latch_dword|=this.plane2[a]<<16;this.latch_dword|=this.plane3[a]<<24;if(this.planar_mode&8)return b=255,this.color_dont_care&1&&(b&=this.plane0[a]^~(this.color_compare&1?255:0)),this.color_dont_care&2&&(b&=
this.plane1[a]^~(this.color_compare&2?255:0)),this.color_dont_care&4&&(b&=this.plane2[a]^~(this.color_compare&4?255:0)),this.color_dont_care&8&&(b&=this.plane3[a]^~(this.color_compare&8?255:0)),b;b=this.plane_read;this.graphical_mode?this.sequencer_memory_mode&8?(b=a&3,a&=-4):this.planar_mode&16&&(b=a&1,a&=-2):b&=3;return this.vga_memory[b<<16|a]};
X.prototype.vga_memory_write=function(a,b){if(this.svga_enabled)this.cpu.write8((a-655360|this.svga_bank_offset)+3758096384|0,b);else{var c=this.miscellaneous_graphics_register>>2&3;a-=Xc[c];0>a||a>=Yc[c]?(y(a>>>0),y(b)):this.graphical_mode?this.vga_memory_write_graphical(a,b):this.plane_write_bm&3?this.vga_memory_write_text_mode(a,b):this.plane_write_bm&4&&(this.plane2[a]=b)}};
X.prototype.vga_memory_write=function(a,b){if(this.svga_enabled)this.cpu.write8((a-655360|this.svga_bank_offset)+3758096384|0,b);else{var c=this.miscellaneous_graphics_register>>2&3;a-=Yc[c];0>a||a>=Zc[c]?(y(a>>>0),y(b)):this.graphical_mode?this.vga_memory_write_graphical(a,b):this.plane_write_bm&3?this.vga_memory_write_text_mode(a,b):this.plane_write_bm&4&&(this.plane2[a]=b)}};
X.prototype.vga_memory_write_graphical=function(a,b){var c=this.planar_mode&3,d=this.apply_feed(this.planar_bitmap),e=this.apply_expand(this.planar_setreset),f=this.apply_expand(this.planar_setreset_enable);switch(c){case 0:b=this.apply_rotate(b);var g=this.apply_feed(b);g=this.apply_setreset(g,f);g=this.apply_logical(g,this.latch_dword);g=this.apply_bitmask(g,d);break;case 1:g=this.latch_dword;break;case 2:g=this.apply_expand(b);g=this.apply_logical(g,this.latch_dword);g=this.apply_bitmask(g,d);
break;case 3:b=this.apply_rotate(b),d&=this.apply_feed(b),g=this.apply_bitmask(e,d)}b=15;switch(this.sequencer_memory_mode&12){case 0:b=5<<(a&1);a&=-2;break;case 8:case 12:b=1<<(a&3),a&=-4}b&=this.plane_write_bm;b&1&&(this.plane0[a]=g>>0&255);b&2&&(this.plane1[a]=g>>8&255);b&4&&(this.plane2[a]=g>>16&255);b&8&&(this.plane3[a]=g>>24&255);a=this.vga_addr_to_pixel(a);this.partial_replot(a,a+7)};X.prototype.apply_feed=function(a){return a|a<<8|a<<16|a<<24};
X.prototype.apply_expand=function(a){return(a&1?255:0)|(a&2?255:0)<<8|(a&4?255:0)<<16|(a&8?255:0)<<24};X.prototype.apply_rotate=function(a){return(a|a<<8)>>>(this.planar_rotate_reg&7)&255};X.prototype.apply_setreset=function(a,b){var c=this.apply_expand(this.planar_setreset);return(a|b&c)&(~b|c)};X.prototype.apply_logical=function(a,b){switch(this.planar_rotate_reg&24){case 8:return a&b;case 16:return a|b;case 24:return a^b}return a};X.prototype.apply_bitmask=function(a,b){return b&a|~b&this.latch_dword};
@@ -577,14 +583,14 @@ X.prototype.vga_addr_to_pixel=function(a){var b=this.vga_addr_shift_count();if(~
X.prototype.scan_line_to_screen_row=function(a){this.max_scan_line&128&&(a>>>=1);a=Math.ceil(a/(1+(this.max_scan_line&31)));this.crtc_mode&1||(a<<=1);this.crtc_mode&2||(a<<=1);return a};X.prototype.set_size_text=function(a,b){this.max_cols=a;this.max_rows=b;this.screen.set_size_text(a,b);this.bus.send("screen-set-size",[a,b,0])};
X.prototype.set_size_graphical=function(a,b,c,d,e){c=Math.max(c,1);d=Math.max(d,1);if(this.screen_width!==a||this.screen_height!==b||this.virtual_width!==c||this.virtual_height!==d){this.screen_width=a;this.screen_height=b;this.virtual_width=c;this.virtual_height=d;if("undefined"!==typeof ImageData){const f=c*d,g=this.cpu.svga_allocate_dest_buffer(f)>>>0;this.dest_buffet_offset=g;this.image_data=new ImageData(new Uint8ClampedArray(this.cpu.wasm_memory.buffer,g,4*f),c,d);this.cpu.svga_mark_dirty()}this.screen.set_size_graphical(a,
b,c,d);this.bus.send("screen-set-size",[a,b,e])}};
X.prototype.update_vga_size=function(){if(!this.svga_enabled){var a=Math.min(1+this.horizontal_display_enable_end,this.horizontal_blank_start),b=Math.min(1+this.vertical_display_enable_end,this.vertical_blank_start);if(a&&b)if(this.graphical_mode){a<<=3;var c=this.offset_register<<4,d=4;this.attribute_mode&64?(a>>>=1,c>>>=1,d=8):this.attribute_mode&2&&(d=1);b=this.scan_line_to_screen_row(b);var e=Yc[0];const f=this.vga_bytes_per_line();this.set_size_graphical(a,b,c,f?Math.ceil(e/f):b,d);this.update_vertical_retrace();
X.prototype.update_vga_size=function(){if(!this.svga_enabled){var a=Math.min(1+this.horizontal_display_enable_end,this.horizontal_blank_start),b=Math.min(1+this.vertical_display_enable_end,this.vertical_blank_start);if(a&&b)if(this.graphical_mode){a<<=3;var c=this.offset_register<<4,d=4;this.attribute_mode&64?(a>>>=1,c>>>=1,d=8):this.attribute_mode&2&&(d=1);b=this.scan_line_to_screen_row(b);var e=Zc[0];const f=this.vga_bytes_per_line();this.set_size_graphical(a,b,c,f?Math.ceil(e/f):b,d);this.update_vertical_retrace();
this.update_layers()}else this.max_scan_line&128&&(b>>>=1),c=b/(1+(this.max_scan_line&31))|0,a&&c&&this.set_size_text(a,c)}};
X.prototype.update_layers=function(){this.graphical_mode||this.text_mode_redraw();if(this.svga_enabled)this.layers=[];else if(this.virtual_width&&this.screen_width)if(!this.palette_source||this.clocking_mode&32)this.layers=[],this.screen.clear_screen();else{var a=this.start_address_latched,b=this.horizontal_panning;this.attribute_mode&64&&(b>>>=1);var c=this.preset_row_scan>>5&3,d=this.vga_addr_to_pixel(a+c);a=d/this.virtual_width|0;var e=d%this.virtual_width+b;d=this.scan_line_to_screen_row(1+this.line_compare);
d=Math.min(d,this.screen_height);var f=this.screen_height-d;this.layers=[];e=-e;for(var g=0;e<this.screen_width;e+=this.virtual_width,g++)this.layers.push({image_data:this.image_data,screen_x:e,screen_y:0,buffer_x:0,buffer_y:a+g,buffer_width:this.virtual_width,buffer_height:d});a=0;this.attribute_mode&32||(a=this.vga_addr_to_pixel(c)+b);e=-a;for(g=0;e<this.screen_width;e+=this.virtual_width,g++)this.layers.push({image_data:this.image_data,screen_x:e,screen_y:d,buffer_x:0,buffer_y:g,buffer_width:this.virtual_width,
buffer_height:f})}};X.prototype.update_vertical_retrace=function(){this.port_3DA_value|=8;this.start_address_latched!==this.start_address&&(this.start_address_latched=this.start_address,this.update_layers())};X.prototype.update_cursor_scanline=function(){var a=this.max_scan_line&31;const b=Math.min(a,this.cursor_scanline_start&31);a=Math.min(a,this.cursor_scanline_end&31);this.screen.update_cursor_scanline(b,a,!(this.cursor_scanline_start&32)&&b<a)};
X.prototype.port3C0_write=function(a){if(-1===this.attribute_controller_index)y(a),this.attribute_controller_index=a&31,y(this.attribute_controller_index),this.palette_source!==(a&32)&&(this.palette_source=a&32,this.update_layers());else{if(16>this.attribute_controller_index)y(this.attribute_controller_index),y(a),this.dac_map[this.attribute_controller_index]=a,this.attribute_mode&64||this.complete_redraw();else switch(this.attribute_controller_index){case 16:y(a);if(this.attribute_mode!==a){var b=
this.attribute_mode;this.attribute_mode=a;const c=0!==(a&1);this.svga_enabled||this.graphical_mode===c||(this.graphical_mode=c,this.screen.set_mode(this.graphical_mode));(b^a)&64&&this.complete_replot();this.update_vga_size();this.complete_redraw();this.set_font_bitmap(!1)}break;case 18:y(a);this.color_plane_enable!==a&&(this.color_plane_enable=a,this.complete_redraw());break;case 19:y(a);this.horizontal_panning!==a&&(this.horizontal_panning=a&15,this.update_layers());break;case 20:y(a);this.color_select!==
a&&(this.color_select=a,this.complete_redraw());break;default:y(this.attribute_controller_index),y(a)}this.attribute_controller_index=-1}};X.prototype.port3C0_read=function(){return(this.attribute_controller_index|this.palette_source)&255};X.prototype.port3C0_read16=function(){return this.port3C0_read()|this.port3C1_read()<<8&65280};
this.attribute_mode;this.attribute_mode=a;!this.svga_enabled||this.dispi_enable_value&1||(this.svga_enabled=!1,this.svga_bank_offset=0);const c=0!==(a&1);this.svga_enabled||this.graphical_mode===c||(this.graphical_mode=c,this.screen.set_mode(this.graphical_mode));(b^a)&64&&this.complete_replot();this.update_vga_size();this.complete_redraw();this.set_font_bitmap(!1)}break;case 18:y(a);this.color_plane_enable!==a&&(this.color_plane_enable=a,this.complete_redraw());break;case 19:y(a);this.horizontal_panning!==
a&&(this.horizontal_panning=a&15,this.update_layers());break;case 20:y(a);this.color_select!==a&&(this.color_select=a,this.complete_redraw());break;default:y(this.attribute_controller_index),y(a)}this.attribute_controller_index=-1}};X.prototype.port3C0_read=function(){return(this.attribute_controller_index|this.palette_source)&255};X.prototype.port3C0_read16=function(){return this.port3C0_read()|this.port3C1_read()<<8&65280};
X.prototype.port3C1_read=function(){if(16>this.attribute_controller_index)return y(this.attribute_controller_index),y(this.dac_map[this.attribute_controller_index]),this.dac_map[this.attribute_controller_index]&255;switch(this.attribute_controller_index){case 16:return y(this.attribute_mode),this.attribute_mode;case 18:return y(this.color_plane_enable),this.color_plane_enable;case 19:return y(this.horizontal_panning),this.horizontal_panning;case 20:return y(this.color_select),this.color_select;default:y(this.attribute_controller_index)}return 255};
X.prototype.port3C2_write=function(a){y(a);this.miscellaneous_output_register=a};X.prototype.port3C4_write=function(a){this.sequencer_index=a};X.prototype.port3C4_read=function(){return this.sequencer_index};
X.prototype.port3C5_write=function(a){switch(this.sequencer_index){case 1:y(a);var b=this.clocking_mode;this.clocking_mode=a;(b^a)&32&&this.update_layers();this.set_font_bitmap(!1);break;case 2:y(a);b=this.plane_write_bm;this.plane_write_bm=a;this.graphical_mode||!(b&4)||this.plane_write_bm&4||this.set_font_bitmap(!0);break;case 3:y(a);b=this.character_map_select;this.character_map_select=a;this.graphical_mode||b===a||this.set_font_page();break;case 4:y(a);this.sequencer_memory_mode=a;break;default:y(this.sequencer_index),
@@ -604,9 +610,10 @@ a,this.update_vga_size(),(b^a)&67&&this.complete_replot());break;case 24:y(a);th
X.prototype.port3D5_read=function(){y(this.index_crtc);switch(this.index_crtc){case 1:return this.horizontal_display_enable_end;case 2:return this.horizontal_blank_start;case 7:return this.vertical_display_enable_end>>7&2|this.vertical_blank_start>>5&8|this.line_compare>>4&16|this.vertical_display_enable_end>>3&64;case 8:return this.preset_row_scan;case 9:return this.max_scan_line;case 10:return this.cursor_scanline_start;case 11:return this.cursor_scanline_end;case 12:return this.start_address&255;
case 13:return this.start_address>>8;case 14:return this.cursor_address>>8;case 15:return this.cursor_address&255;case 18:return this.vertical_display_enable_end&255;case 19:return this.offset_register;case 20:return this.underline_location_register;case 21:return this.vertical_blank_start&255;case 23:return this.crtc_mode;case 24:return this.line_compare&255}return this.index_crtc<this.crtc.length?this.crtc[this.index_crtc]:0};X.prototype.port3D5_read16=function(){return this.port3D5_read()};
X.prototype.port3DA_read=function(){var a=this.port_3DA_value;this.graphical_mode?(this.port_3DA_value^=1,this.port_3DA_value&=1):(this.port_3DA_value&1&&(this.port_3DA_value^=8),this.port_3DA_value^=1);this.attribute_controller_index=-1;return a};X.prototype.port1CE_write=function(a){this.dispi_index=a};
X.prototype.port1CF_write=function(a){y(this.dispi_index);y(a);const b=this.svga_enabled;switch(this.dispi_index){case 0:45248<=a&&45253>=a?this.svga_version=a:y(a);break;case 1:this.svga_width=a;2560<this.svga_width&&(this.svga_width=2560);break;case 2:this.svga_height=a;1600<this.svga_height&&(this.svga_height=1600);break;case 3:this.svga_bpp=a;break;case 4:(this.svga_enabled=1===(a&1))&&0===(a&128)&&this.svga_memory.fill(0);this.dispi_enable_value=a;break;case 5:y(a<<16);this.svga_bank_offset=
a<<16;break;case 8:y(a);this.svga_offset_x!==a&&(this.svga_offset_x=a,this.svga_offset=this.svga_offset_y*this.svga_width+this.svga_offset_x,this.complete_redraw());break;case 9:y(a*this.svga_width);y(a);this.svga_offset_y!==a&&(this.svga_offset_y=a,this.svga_offset=this.svga_offset_y*this.svga_width+this.svga_offset_x,this.complete_redraw());break;default:y(this.dispi_index)}!this.svga_enabled||this.svga_width&&this.svga_height||(this.svga_enabled=!1);this.svga_enabled&&!b&&(this.svga_offset_y=this.svga_offset_x=
this.svga_offset=0,this.graphical_mode=!0,this.screen.set_mode(this.graphical_mode),this.set_size_graphical(this.svga_width,this.svga_height,this.svga_width,this.svga_height,this.svga_bpp));b&&!this.svga_enabled&&(this.graphical_mode=a=0!==(this.attribute_mode&1),this.screen.set_mode(a),this.update_vga_size(),this.set_font_bitmap(!1),this.complete_redraw());this.svga_enabled||(this.svga_bank_offset=0);this.update_layers()};X.prototype.port1CF_read=function(){y(this.dispi_index);return this.svga_register_read(this.dispi_index)};
X.prototype.port1CF_write=function(a){y(this.dispi_index);y(a);const b=this.svga_enabled;switch(this.dispi_index){case 0:45248<=a&&45253>=a?this.svga_version=a:y(a);break;case 1:this.svga_width=a;2560<this.svga_width&&(this.svga_width=2560);break;case 2:this.svga_height=a;1600<this.svga_height&&(this.svga_height=1600);break;case 3:this.svga_bpp=a;break;case 4:if(!(a&1)&&this.svga_enabled&&this.cpu.flags[0]&131072){this.dispi_enable_value=a;break}(this.svga_enabled=1===(a&1))&&0===(a&128)&&this.svga_memory.fill(0);
this.dispi_enable_value=a;break;case 5:y(a<<16);this.svga_bank_offset=a<<16;break;case 8:y(a);this.svga_offset_x!==a&&(this.svga_offset_x=a,this.svga_offset=this.svga_offset_y*this.svga_width+this.svga_offset_x,this.complete_redraw());break;case 9:y(a*this.svga_width);y(a);this.svga_offset_y!==a&&(this.svga_offset_y=a,this.svga_offset=this.svga_offset_y*this.svga_width+this.svga_offset_x,this.complete_redraw());break;default:y(this.dispi_index)}!this.svga_enabled||this.svga_width&&this.svga_height||
(this.svga_enabled=!1);this.svga_enabled&&4===this.dispi_index&&(b||(this.svga_offset_y=this.svga_offset_x=this.svga_offset=0),this.graphical_mode=!0,this.screen.set_mode(this.graphical_mode),this.set_size_graphical(this.svga_width,this.svga_height,this.svga_width,this.svga_height,this.svga_bpp));b&&!this.svga_enabled&&(this.graphical_mode=a=0!==(this.attribute_mode&1),this.screen.set_mode(a),this.update_vga_size(),this.set_font_bitmap(!1),this.complete_redraw());this.svga_enabled||(this.svga_bank_offset=
0);this.update_layers()};X.prototype.port1CF_read=function(){y(this.dispi_index);return this.svga_register_read(this.dispi_index)};
X.prototype.svga_register_read=function(a){switch(a){case 0:return this.svga_version;case 1:return this.dispi_enable_value&2?2560:this.svga_width;case 2:return this.dispi_enable_value&2?1600:this.svga_height;case 3:return this.dispi_enable_value&2?32:this.svga_bpp;case 4:return this.dispi_enable_value;case 5:return this.svga_bank_offset>>>16;case 6:return this.screen_width?this.screen_width:1;case 8:return this.svga_offset_x;case 9:return this.svga_offset_y;case 10:return this.vga_memory_size/65536|
0;default:y(this.dispi_index)}return 255};
X.prototype.vga_replot=function(){for(var a=this.diff_plot_min&-16,b=Math.min(this.diff_plot_max|15,524287),c=this.vga_addr_shift_count(),d=~this.crtc_mode&3,e=this.planar_mode&96,f=this.attribute_mode&64;a<=b;){var g=a>>>c;if(d){var h=a/this.virtual_width|0,l=a-this.virtual_width*h;switch(d){case 1:g=(h&1)<<13;h>>>=1;break;case 2:g=(h&1)<<14;h>>>=1;break;case 3:g=(h&3)<<13,h>>>=2}g|=(h*this.virtual_width+l>>>c)+this.start_address}h=this.plane0[g];l=this.plane1[g];var m=this.plane2[g],n=this.plane3[g];
@@ -618,13 +625,13 @@ X.prototype.screen_fill_buffer=function(){if(this.graphical_mode){if(0===this.im
f=new Uint8Array(this.cpu.wasm_memory.buffer,this.svga_memory.byteOffset,this.vga_memory_size);for(var b=0;b<e.length;b++){var c=this.vga256_palette[f[b]];e[b]=c&65280|c<<16|c>>16|4278190080}}else this.cpu.svga_fill_pixel_buffer(this.svga_bpp,this.svga_offset),b=15===this.svga_bpp?2:this.svga_bpp/8,a=((this.cpu.svga_dirty_bitmap_min_offset[0]/b|0)-this.svga_offset)/this.svga_width|0,d=(((this.cpu.svga_dirty_bitmap_max_offset[0]/b|0)-this.svga_offset)/this.svga_width|0)+1;a<d&&(a=Math.max(a,0),d=Math.min(d,
this.svga_height),this.screen.update_buffer([{image_data:this.image_data,screen_x:0,screen_y:a,buffer_x:0,buffer_y:a,buffer_width:this.svga_width,buffer_height:d-a}]))}else this.vga_replot(),this.vga_redraw(),this.screen.update_buffer(this.layers);this.reset_diffs()}this.update_vertical_retrace()};
X.prototype.set_font_bitmap=function(a){const b=this.max_scan_line&31;if(b&&!this.graphical_mode){const c=!!(this.clocking_mode&8);this.screen.set_font_bitmap(b+1,!c&&!(this.clocking_mode&1),c,!!(this.attribute_mode&4),this.plane2,a)}};
X.prototype.set_font_page=function(){const a=[0,2,4,6,1,3,5,7],b=(this.character_map_select&12)>>2|(this.character_map_select&32)>>3,c=this.character_map_select&3|(this.character_map_select&16)>>2;this.font_page_ab_enabled=b!==c;this.screen.set_font_page(a[b],a[c]);this.complete_redraw()};const Zc="SWAP_IN SWAP_OUT MAJFLT MINFLT MEMFREE MEMTOT AVAIL CACHES HTLB_PGALLOC HTLB_PGFAIL".split(" ");
function $c(a,b){this.bus=b;this.zeroed=this.fp_cmd=this.actual=this.num_pages=0;this.virtio=new Fc(a,{name:"virtio-balloon",pci_id:88,device_id:4165,subsystem_device_id:5,common:{initial_port:55296,queues:[{size_supported:32,notify_offset:0},{size_supported:32,notify_offset:0},{size_supported:2,notify_offset:1},{size_supported:64,notify_offset:2}],features:[1,3,32],on_driver_ok:()=>{}},notification:{initial_port:55552,single_handler:!1,handlers:[c=>{const d=this.virtio.queues[c];for(;d.has_request();){var e=
d.pop_request();const f=new Uint8Array(e.length_readable);e.get_next_blob(f);this.virtio.queues[c].push_reply(e);e=f.byteLength/4;this.actual+=0===c?e:-e}this.virtio.queues[c].flush_replies()},c=>{var d=this.virtio.queues[c];if(d.has_request()){d=d.pop_request();const e=new Uint8Array(d.length_readable);d.get_next_blob(e);let f={};for(let g=0;g<d.length_readable;g+=10){let [h,l]=I(["h","d"],e,{offset:g});f[Zc[h]]=l}this.virtio.queues[c].push_reply(d);this.stats_cb&&this.stats_cb(f)}},c=>{const d=
X.prototype.set_font_page=function(){const a=[0,2,4,6,1,3,5,7],b=(this.character_map_select&12)>>2|(this.character_map_select&32)>>3,c=this.character_map_select&3|(this.character_map_select&16)>>2;this.font_page_ab_enabled=b!==c;this.screen.set_font_page(a[b],a[c]);this.complete_redraw()};const $c="SWAP_IN SWAP_OUT MAJFLT MINFLT MEMFREE MEMTOT AVAIL CACHES HTLB_PGALLOC HTLB_PGFAIL".split(" ");
function ad(a,b){this.bus=b;this.zeroed=this.fp_cmd=this.actual=this.num_pages=0;this.virtio=new Fc(a,{name:"virtio-balloon",pci_id:88,device_id:4165,subsystem_device_id:5,common:{initial_port:55296,queues:[{size_supported:32,notify_offset:0},{size_supported:32,notify_offset:0},{size_supported:2,notify_offset:1},{size_supported:64,notify_offset:2}],features:[1,3,32],on_driver_ok:()=>{}},notification:{initial_port:55552,single_handler:!1,handlers:[c=>{const d=this.virtio.queues[c];for(;d.has_request();){var e=
d.pop_request();const f=new Uint8Array(e.length_readable);e.get_next_blob(f);this.virtio.queues[c].push_reply(e);e=f.byteLength/4;this.actual+=0===c?e:-e}this.virtio.queues[c].flush_replies()},c=>{var d=this.virtio.queues[c];if(d.has_request()){d=d.pop_request();const e=new Uint8Array(d.length_readable);d.get_next_blob(e);let f={};for(let g=0;g<d.length_readable;g+=10){let [h,l]=I(["h","d"],e,{offset:g});f[$c[h]]=l}this.virtio.queues[c].push_reply(d);this.stats_cb&&this.stats_cb(f)}},c=>{const d=
this.virtio.queues[c];for(;d.has_request();){const f=d.pop_request();if(0<f.length_readable){var e=new Uint8Array(f.length_readable);f.get_next_blob(e);[e]=I(["w"],e,{offset:0});0===e&&(this.free_cb&&this.free_cb(this.zeroed),1<this.fp_cmd&&(this.fp_cmd=1),this.virtio.notify_config_changes())}if(0<f.length_writable)for(new Uint8Array(0),e=0;e<f.write_buffers.length;++e){let g=f.write_buffers[e];this.zeroed+=g.len;this.virtio.cpu.zero_memory(g.addr_low,g.len)}this.virtio.queues[c].push_reply(f)}this.virtio.queues[c].flush_replies()}]},
isr_status:{initial_port:55040},device_specific:{initial_port:54784,struct:[{bytes:4,name:"num_pages",read:()=>this.num_pages,write:()=>{}},{bytes:4,name:"actual",read:()=>this.actual,write:()=>{}},{bytes:4,name:"free_page_hint_cmd_id",read:()=>this.fp_cmd,write:()=>{}}]}})}$c.prototype.Inflate=function(a){this.num_pages+=a;this.virtio.notify_config_changes()};$c.prototype.Deflate=function(a){this.num_pages-=a;this.virtio.notify_config_changes()};
$c.prototype.Cleanup=function(a){this.fp_cmd=2;this.free_cb=a;this.zeroed=0;this.virtio.notify_config_changes()};$c.prototype.get_state=function(){const a=[];a[0]=this.virtio;a[1]=this.num_pages;a[2]=this.actual;return a};$c.prototype.set_state=function(a){this.virtio.set_state(a[0]);this.num_pages=a[1];this.actual=a[2]};$c.prototype.GetStats=function(a){this.stats_cb=a;for(a=this.virtio.queues[2];a.has_request();){const b=a.pop_request();this.virtio.queues[2].push_reply(b)}this.virtio.queues[2].flush_replies()};
$c.prototype.Reset=function(){};function ad(a,b,c,d){var e=new Uint8Array(b);const f=new Uint16Array(b);var g=new Uint32Array(b),h=e[497]||4,l=f[255];if(43605!==l)y(l);else if(l=f[257]|f[258]<<16,1400005704!==l)y(l);else{l=f[259];var m=e[529],n=f[283],p=g[139],q=g[140],r=e[565],x=518<=l?g[142]:255,C=g[146],t=g[147],A=g[150],M=g[151],v=g[152];y(l);y(m);y(n);y(g[133]);y(p);y(q);y(r);y(x);y(C);y(t);y(M);y(A);y(v);e[528]=255;e[529]=m&-97|128;f[274]=56832;f[253]=65535;y(56832);d+="\x00";y(581632);g[138]=581632;for(e=0;e<d.length;e++)a[581632+
isr_status:{initial_port:55040},device_specific:{initial_port:54784,struct:[{bytes:4,name:"num_pages",read:()=>this.num_pages,write:()=>{}},{bytes:4,name:"actual",read:()=>this.actual,write:()=>{}},{bytes:4,name:"free_page_hint_cmd_id",read:()=>this.fp_cmd,write:()=>{}}]}})}ad.prototype.Inflate=function(a){this.num_pages+=a;this.virtio.notify_config_changes()};ad.prototype.Deflate=function(a){this.num_pages-=a;this.virtio.notify_config_changes()};
ad.prototype.Cleanup=function(a){this.fp_cmd=2;this.free_cb=a;this.zeroed=0;this.virtio.notify_config_changes()};ad.prototype.get_state=function(){const a=[];a[0]=this.virtio;a[1]=this.num_pages;a[2]=this.actual;return a};ad.prototype.set_state=function(a){this.virtio.set_state(a[0]);this.num_pages=a[1];this.actual=a[2]};ad.prototype.GetStats=function(a){this.stats_cb=a;for(a=this.virtio.queues[2];a.has_request();){const b=a.pop_request();this.virtio.queues[2].push_reply(b)}this.virtio.queues[2].flush_replies()};
ad.prototype.Reset=function(){};function bd(a,b,c,d){var e=new Uint8Array(b);const f=new Uint16Array(b);var g=new Uint32Array(b),h=e[497]||4,l=f[255];if(43605!==l)y(l);else if(l=f[257]|f[258]<<16,1400005704!==l)y(l);else{l=f[259];var m=e[529],n=f[283],p=g[139],q=g[140],r=e[565],x=518<=l?g[142]:255,C=g[146],t=g[147],A=g[150],M=g[151],v=g[152];y(l);y(m);y(n);y(g[133]);y(p);y(q);y(r);y(x);y(C);y(t);y(M);y(A);y(v);e[528]=255;e[529]=m&-97|128;f[274]=56832;f[253]=65535;y(56832);d+="\x00";y(581632);g[138]=581632;for(e=0;e<d.length;e++)a[581632+
e]=d.charCodeAt(e);h=512*(h+1);y(h);d=new Uint8Array(b,0,h);b=new Uint8Array(b,h);e=h=0;c&&(h=67108864,e=c.byteLength,a.set(new Uint8Array(c),h));g[134]=h;g[135]=e;a.set(d,524288);a.set(b,1048576);a=new Uint8Array(512);(new Uint16Array(a.buffer))[0]=43605;a[2]=1;c=3;a[c++]=250;a[c++]=184;a[c++]=32768;a[c++]=128;a[c++]=142;a[c++]=192;a[c++]=142;a[c++]=216;a[c++]=142;a[c++]=224;a[c++]=142;a[c++]=232;a[c++]=142;a[c++]=208;a[c++]=188;a[c++]=57344;a[c++]=224;a[c++]=234;a[c++]=0;a[c++]=0;a[c++]=32800;a[c++]=
128;g=a[c]=0;for(b=0;b<a.length;b++)g+=a[b];a[c]=-g;return{name:"genroms/kernel.bin",data:a}}};function O(a,b,c){this.stop_idling=c;this.wm=b;this.wasm_patch();this.create_jit_imports();this.wasm_memory=b=this.wm.exports.memory;this.memory_size=k(Uint32Array,b,812,1);this.mem8=new Uint8Array(0);this.mem32s=new Int32Array(this.mem8.buffer);this.segment_is_null=k(Uint8Array,b,724,8);this.segment_offsets=k(Int32Array,b,736,8);this.segment_limits=k(Uint32Array,b,768,8);this.segment_access_bytes=k(Uint8Array,b,512,8);this.protected_mode=k(Int32Array,b,800,1);this.idtr_size=k(Int32Array,b,564,1);
this.idtr_offset=k(Int32Array,b,568,1);this.gdtr_size=k(Int32Array,b,572,1);this.gdtr_offset=k(Int32Array,b,576,1);this.tss_size_32=k(Int32Array,b,1128,1);this.page_fault=k(Uint32Array,b,540,8);this.cr=k(Int32Array,b,580,8);this.cpl=k(Uint8Array,b,612,1);this.is_32=k(Int32Array,b,804,1);this.stack_size_32=k(Int32Array,b,808,1);this.in_hlt=k(Uint8Array,b,616,1);this.last_virt_eip=k(Int32Array,b,620,1);this.eip_phys=k(Int32Array,b,624,1);this.sysenter_cs=k(Int32Array,b,636,1);this.sysenter_esp=k(Int32Array,
@@ -643,16 +650,16 @@ O.prototype.jit_force_generate=function(a){this.jit_force_generate_unsafe&&this.
O.prototype.get_state=function(){var a=[];a[0]=this.memory_size[0];a[1]=new Uint8Array([...this.segment_is_null,...this.segment_access_bytes]);a[2]=this.segment_offsets;a[3]=this.segment_limits;a[4]=this.protected_mode[0];a[5]=this.idtr_offset[0];a[6]=this.idtr_size[0];a[7]=this.gdtr_offset[0];a[8]=this.gdtr_size[0];a[9]=this.page_fault[0];a[10]=this.cr;a[11]=this.cpl[0];a[13]=this.is_32[0];a[16]=this.stack_size_32[0];a[17]=this.in_hlt[0];a[18]=this.last_virt_eip[0];a[19]=this.eip_phys[0];a[22]=this.sysenter_cs[0];
a[23]=this.sysenter_eip[0];a[24]=this.sysenter_esp[0];a[25]=this.prefixes[0];a[26]=this.flags[0];a[27]=this.flags_changed[0];a[28]=this.last_op1[0];a[30]=this.last_op_size[0];a[37]=this.instruction_pointer[0];a[38]=this.previous_ip[0];a[39]=this.reg32;a[40]=this.sreg;a[41]=this.dreg;a[42]=this.reg_pdpte;this.store_current_tsc();a[43]=this.current_tsc;a[45]=this.devices.virtio_9p;a[46]=this.get_state_apic();a[47]=this.devices.rtc;a[48]=this.devices.pci;a[49]=this.devices.dma;a[50]=this.devices.acpi;
a[52]=this.devices.vga;a[53]=this.devices.ps2;a[54]=this.devices.uart0;a[55]=this.devices.fdc;this.devices.ide.secondary?a[85]=this.devices.ide:this.devices.ide.primary?.master.is_atapi?a[56]=this.devices.ide.primary:a[57]=this.devices.ide.primary;a[58]=this.devices.pit;a[59]=this.devices.net;a[60]=this.get_state_pic();a[61]=this.devices.sb16;a[62]=this.fw_value;a[63]=this.get_state_ioapic();a[64]=this.tss_size_32[0];a[66]=this.reg_xmm32s;a[67]=this.fpu_st;a[68]=this.fpu_stack_empty[0];a[69]=this.fpu_stack_ptr[0];
a[70]=this.fpu_control_word[0];a[71]=this.fpu_ip[0];a[72]=this.fpu_ip_selector[0];a[73]=this.fpu_dp[0];a[74]=this.fpu_dp_selector[0];a[75]=this.fpu_opcode[0];const {packed_memory:b,bitmap:c}=this.pack_memory();a[77]=b;a[78]=new Uint8Array(c.get_buffer());a[79]=this.devices.uart1;a[80]=this.devices.uart2;a[81]=this.devices.uart3;a[82]=this.devices.virtio_console;a[83]=this.devices.virtio_net;a[84]=this.devices.virtio_balloon;a[86]=this.last_result;a[87]=this.fpu_status_word;a[88]=this.mxcsr;return a};
O.prototype.get_state_pic=function(){const a=new Uint8Array(this.wasm_memory.buffer,this.get_pic_addr_master(),13),b=new Uint8Array(this.wasm_memory.buffer,this.get_pic_addr_slave(),13),c=[],d=[];c[0]=a[0];c[1]=a[1];c[2]=a[2];c[3]=a[3];c[4]=a[4];c[5]=d;c[6]=a[6];c[7]=a[7];c[8]=a[8];c[9]=a[9];c[10]=a[10];c[11]=a[11];c[12]=a[12];d[0]=b[0];d[1]=b[1];d[2]=b[2];d[3]=b[3];d[4]=b[4];d[5]=null;d[6]=b[6];d[7]=b[7];d[8]=b[8];d[9]=b[9];d[10]=b[10];d[11]=b[11];d[12]=b[12];return c};
a[70]=this.fpu_control_word[0];a[71]=this.fpu_ip[0];a[72]=this.fpu_ip_selector[0];a[73]=this.fpu_dp[0];a[74]=this.fpu_dp_selector[0];a[75]=this.fpu_opcode[0];const {packed_memory:b,bitmap:c}=this.pack_memory();a[77]=b;a[78]=new Uint8Array(c.get_buffer());a[79]=this.devices.uart1;a[80]=this.devices.uart2;a[81]=this.devices.uart3;a[82]=this.devices.virtio_console;a[83]=this.devices.virtio_net;a[84]=this.devices.virtio_balloon;a[86]=this.last_result;a[87]=this.fpu_status_word;a[88]=this.mxcsr;a[89]=
this.devices.vmware;return a};O.prototype.get_state_pic=function(){const a=new Uint8Array(this.wasm_memory.buffer,this.get_pic_addr_master(),13),b=new Uint8Array(this.wasm_memory.buffer,this.get_pic_addr_slave(),13),c=[],d=[];c[0]=a[0];c[1]=a[1];c[2]=a[2];c[3]=a[3];c[4]=a[4];c[5]=d;c[6]=a[6];c[7]=a[7];c[8]=a[8];c[9]=a[9];c[10]=a[10];c[11]=a[11];c[12]=a[12];d[0]=b[0];d[1]=b[1];d[2]=b[2];d[3]=b[3];d[4]=b[4];d[5]=null;d[6]=b[6];d[7]=b[7];d[8]=b[8];d[9]=b[9];d[10]=b[10];d[11]=b[11];d[12]=b[12];return c};
O.prototype.get_state_apic=function(){return new Uint8Array(this.wasm_memory.buffer,this.get_apic_addr(),184)};O.prototype.get_state_ioapic=function(){return new Uint8Array(this.wasm_memory.buffer,this.get_ioapic_addr(),208)};
O.prototype.set_state=function(a){this.memory_size[0]=a[0];this.mem8.length!==this.memory_size[0]&&console.warn("Note: Memory size mismatch. we="+this.mem8.length+" state="+this.memory_size[0]);8===a[1].length?(this.segment_is_null.set(a[1]),this.segment_access_bytes.fill(242),this.segment_access_bytes[1]=250):16===a[1].length&&(this.segment_is_null.set(a[1].subarray(0,8)),this.segment_access_bytes.set(a[1].subarray(8,16)));this.segment_offsets.set(a[2]);this.segment_limits.set(a[3]);this.protected_mode[0]=
a[4];this.idtr_offset[0]=a[5];this.idtr_size[0]=a[6];this.gdtr_offset[0]=a[7];this.gdtr_size[0]=a[8];this.page_fault[0]=a[9];this.cr.set(a[10]);this.cpl[0]=a[11];this.is_32[0]=a[13];this.stack_size_32[0]=a[16];this.in_hlt[0]=a[17];this.last_virt_eip[0]=a[18];this.eip_phys[0]=a[19];this.sysenter_cs[0]=a[22];this.sysenter_eip[0]=a[23];this.sysenter_esp[0]=a[24];this.prefixes[0]=a[25];this.flags[0]=a[26];this.flags_changed[0]=a[27];this.last_op1[0]=a[28];this.last_op_size[0]=a[30];this.instruction_pointer[0]=
a[37];this.previous_ip[0]=a[38];this.reg32.set(a[39]);this.sreg.set(a[40]);this.dreg.set(a[41]);a[42]&&this.reg_pdpte.set(a[42]);this.set_tsc(a[43][0],a[43][1]);this.devices.virtio_9p&&this.devices.virtio_9p.set_state(a[45]);a[46]&&this.set_state_apic(a[46]);this.devices.rtc&&this.devices.rtc.set_state(a[47]);this.devices.dma&&this.devices.dma.set_state(a[49]);this.devices.acpi&&this.devices.acpi.set_state(a[50]);this.devices.vga&&this.devices.vga.set_state(a[52]);this.devices.ps2&&this.devices.ps2.set_state(a[53]);
this.devices.uart0&&this.devices.uart0.set_state(a[54]);this.devices.fdc&&this.devices.fdc.set_state(a[55]);if(a[56]||a[57]){var b=[[void 0,void 0],[void 0,void 0]];b[0][0]=a[56]?{is_cdrom:!0,buffer:this.devices.cdrom.buffer}:{is_cdrom:!1,buffer:this.devices.ide.primary.master.buffer};this.devices.ide=new Uc(this,this.devices.ide.bus,b);this.devices.cdrom=a[56]?this.devices.ide.primary.master:void 0;this.devices.ide.primary.set_state(a[56]||a[57])}else a[85]&&this.devices.ide.set_state(a[85]);this.devices.pci&&
this.devices.uart0&&this.devices.uart0.set_state(a[54]);this.devices.fdc&&this.devices.fdc.set_state(a[55]);if(a[56]||a[57]){var b=[[void 0,void 0],[void 0,void 0]];b[0][0]=a[56]?{is_cdrom:!0,buffer:this.devices.cdrom.buffer}:{is_cdrom:!1,buffer:this.devices.ide.primary.master.buffer};this.devices.ide=new Vc(this,this.devices.ide.bus,b);this.devices.cdrom=a[56]?this.devices.ide.primary.master:void 0;this.devices.ide.primary.set_state(a[56]||a[57])}else a[85]&&this.devices.ide.set_state(a[85]);this.devices.pci&&
this.devices.pci.set_state(a[48]);this.devices.pit&&this.devices.pit.set_state(a[58]);this.devices.net&&this.devices.net.set_state(a[59]);this.set_state_pic(a[60]);this.devices.sb16&&this.devices.sb16.set_state(a[61]);this.devices.uart1&&this.devices.uart1.set_state(a[79]);this.devices.uart2&&this.devices.uart2.set_state(a[80]);this.devices.uart3&&this.devices.uart3.set_state(a[81]);this.devices.virtio_console&&this.devices.virtio_console.set_state(a[82]);this.devices.virtio_net&&this.devices.virtio_net.set_state(a[83]);
this.devices.virtio_balloon&&this.devices.virtio_balloon.set_state(a[84]);this.fw_value=a[62];a[63]&&this.set_state_ioapic(a[63]);this.tss_size_32[0]=a[64];this.reg_xmm32s.set(a[66]);this.fpu_st.set(a[67]);this.fpu_stack_empty[0]=a[68];this.fpu_stack_ptr[0]=a[69];this.fpu_control_word[0]=a[70];this.fpu_ip[0]=a[71];this.fpu_ip_selector[0]=a[72];this.fpu_dp[0]=a[73];this.fpu_dp_selector[0]=a[74];this.fpu_opcode[0]=a[75];void 0!==a[86]&&(this.last_result=a[86]);void 0!==a[87]&&(this.fpu_status_word=
a[87]);void 0!==a[88]&&(this.mxcsr=a[88]);b=new ma(a[78].buffer);this.unpack_memory(b,a[77]);this.update_state_flags();this.full_clear_tlb();this.jit_clear_cache()};
this.devices.virtio_balloon&&this.devices.virtio_balloon.set_state(a[84]);this.devices.vmware&&a[89]&&this.devices.vmware.set_state(a[89]);this.fw_value=a[62];a[63]&&this.set_state_ioapic(a[63]);this.tss_size_32[0]=a[64];this.reg_xmm32s.set(a[66]);this.fpu_st.set(a[67]);this.fpu_stack_empty[0]=a[68];this.fpu_stack_ptr[0]=a[69];this.fpu_control_word[0]=a[70];this.fpu_ip[0]=a[71];this.fpu_ip_selector[0]=a[72];this.fpu_dp[0]=a[73];this.fpu_dp_selector[0]=a[74];this.fpu_opcode[0]=a[75];void 0!==a[86]&&
(this.last_result=a[86]);void 0!==a[87]&&(this.fpu_status_word=a[87]);void 0!==a[88]&&(this.mxcsr=a[88]);b=new ma(a[78].buffer);this.unpack_memory(b,a[77]);this.update_state_flags();this.full_clear_tlb();this.jit_clear_cache()};
O.prototype.set_state_pic=function(a){const b=new Uint8Array(this.wasm_memory.buffer,this.get_pic_addr_master(),13),c=new Uint8Array(this.wasm_memory.buffer,this.get_pic_addr_slave(),13);b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];const d=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=a[12];c[0]=d[0];c[1]=d[1];c[2]=d[2];c[3]=d[3];c[4]=d[4];c[6]=d[6];c[7]=d[7];c[8]=d[8];c[9]=d[9];c[10]=d[10];c[11]=d[11];c[12]=d[12]};
O.prototype.set_state_apic=function(a){if(a instanceof Array){const b=new Int32Array(this.wasm_memory.buffer,this.get_apic_addr(),46);b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[8]=a[6];b[9]=a[7];b[10]=a[8];b[11]=a[9];b[12]=a[10];b[13]=a[11];b[14]=a[12];b[15]=a[13];b.set(a[15],16);b.set(a[15],24);b.set(a[16],32);b[40]=a[17];b[41]=a[18];b[42]=a[19];b[43]=a[20];b[44]=a[21];b[45]=a[22]||65536}else(new Uint8Array(this.wasm_memory.buffer,this.get_apic_addr(),184)).set(a)};
O.prototype.set_state_ioapic=function(a){if(a instanceof Array){const b=new Int32Array(this.wasm_memory.buffer,this.get_ioapic_addr(),52);b.set(a[0],0);b.set(a[1],24);b[48]=a[2];b[49]=a[3];b[50]=a[4];b[51]=a[5]}else(new Uint8Array(this.wasm_memory.buffer,this.get_ioapic_addr(),208)).set(a)};
@@ -660,17 +667,17 @@ O.prototype.pack_memory=function(){var a=this.mem8.length>>12,b=[];for(var c=0;c
O.prototype.unpack_memory=function(a,b){this.zero_memory(0,this.memory_size[0]);const c=this.memory_size[0]>>12;let d=0;for(let f=0;f<c;f++)if(a.get(f)){var e=d<<12;e=b.subarray(e,e+4096);this.mem8.set(e,f<<12);d++}};
O.prototype.reboot_internal=function(){this.reset_cpu();this.fw_value=[];this.devices.virtio_9p&&this.devices.virtio_9p.reset();this.devices.virtio_console&&this.devices.virtio_console.reset();this.devices.virtio_net&&this.devices.virtio_net.reset();this.devices.ps2&&this.devices.ps2.reset();this.load_bios()};O.prototype.reset_memory=function(){this.mem8.fill(0)};
O.prototype.create_memory=function(a,b){a<b?a=b:0>(a|0)&&(a=Math.pow(2,31)-131072);a=(a-1|131071)+1|0;console.assert(0===this.memory_size[0],"Expected uninitialised memory");this.memory_size[0]=a;b=this.allocate_memory(a);this.mem8=k(Uint8Array,this.wasm_memory,b,a);this.mem32s=k(Uint32Array,this.wasm_memory,b,a>>2)};
O.prototype.init=function(a,b){this.create_memory(a.memory_size||67108864,a.initrd?67108864:1048576);a.disable_jit&&this.set_jit_config(0,1);a.cpuid_level&&this.set_cpuid_level(a.cpuid_level);this.acpi_enabled[0]=+a.acpi;this.reset_cpu();var c=new Ca(this);this.io=c;this.bios.main=a.bios;this.bios.vga=a.vga_bios;this.load_bios();if(a.bzimage){const e=ad(this.mem8,a.bzimage,a.initrd,a.cmdline||"");e&&this.option_roms.push(e)}c.register_read(179,this,function(){return 0});var d=0;c.register_read(146,
O.prototype.init=function(a,b){this.create_memory(a.memory_size||67108864,a.initrd?67108864:1048576);a.disable_jit&&this.set_jit_config(0,1);a.cpuid_level&&this.set_cpuid_level(a.cpuid_level);this.acpi_enabled[0]=+a.acpi;this.reset_cpu();var c=new Ca(this);this.io=c;this.bios.main=a.bios;this.bios.vga=a.vga_bios;this.load_bios();if(a.bzimage){const e=bd(this.mem8,a.bzimage,a.initrd,a.cmdline||"");e&&this.option_roms.push(e)}c.register_read(179,this,function(){return 0});var d=0;c.register_read(146,
this,function(){return d});c.register_write(146,this,function(e){d=e});c.register_read(1297,this,function(){return this.fw_pointer<this.fw_value.length?this.fw_value[this.fw_pointer++]:0});c.register_write(1296,this,void 0,function(e){function f(l){return new Uint8Array(Int32Array.of(l).buffer)}function g(l){return l>>8|l<<8&65280}function h(l){return l<<24|l<<8&16711680|l>>8&65280|l>>>24}ua("bios config port, index="+y(e));this.fw_pointer=0;if(0===e)this.fw_value=f(1431127377);else if(1===e)this.fw_value=
f(0);else if(3===e)this.fw_value=f(this.memory_size[0]);else if(5===e)this.fw_value=f(1);else if(15===e)this.fw_value=f(1);else if(13===e)this.fw_value=new Uint8Array(16);else if(25===e){e=new Int32Array(4+64*this.option_roms.length);const l=new Uint8Array(e.buffer);e[0]=h(this.option_roms.length);for(let m=0;m<this.option_roms.length;m++){const {name:n,data:p}=this.option_roms[m],q=4+64*m;e[q+0>>2]=h(p.length);e[q+4>>2]=g(49152+m);for(let r=0;r<n.length;r++)l[q+8+r]=n.charCodeAt(r)}this.fw_value=
l}else 32768<=e&&49152>e?this.fw_value=f(0):49152<=e&&e-49152<this.option_roms.length?this.fw_value=this.option_roms[e-49152].data:(ua("Warning: Unimplemented fw index: "+y(e)),this.fw_value=f(0))});this.devices={};a.load_devices&&(this.devices.pci=new Ac(this),this.acpi_enabled[0]&&(this.devices.acpi=new yc(this)),this.devices.rtc=new gb(this),this.fill_cmos(this.devices.rtc,a),this.devices.dma=new B(this),this.devices.vga=new X(this,b,a.screen,a.vga_memory_size||8388608),this.devices.ps2=new Gc(this,
b),this.devices.uart0=new zc(this,1016,b),a.uart1&&(this.devices.uart1=new zc(this,760,b)),a.uart2&&(this.devices.uart2=new zc(this,1E3,b)),a.uart3&&(this.devices.uart3=new zc(this,744,b)),this.devices.fdc=new V(this,a.fda,a.fdb),c=[[void 0,void 0],[void 0,void 0]],a.hda&&(c[0][0]={buffer:a.hda},a.hdb&&(c[0][1]={buffer:a.hdb})),c[1][0]={is_cdrom:!0,buffer:a.cdrom},this.devices.ide=new Uc(this,b,c),this.devices.cdrom=this.devices.ide.secondary.master,this.devices.pit=new hb(this,b),"ne2k"===a.net_device.type?
this.devices.net=new Dc(this,b,a.preserve_mac_from_state_image,a.mac_address_translation):"virtio"===a.net_device.type&&(this.devices.virtio_net=new Wc(this,b,a.preserve_mac_from_state_image,a.net_device.mtu)),a.fs9p?this.devices.virtio_9p=new bd(a.fs9p,this,b):a.handle9p?this.devices.virtio_9p=new cd(a.handle9p,this):a.proxy9p&&(this.devices.virtio_9p=new dd(a.proxy9p,this)),a.virtio_console&&(this.devices.virtio_console=new Ec(this,b)),a.virtio_balloon&&(this.devices.virtio_balloon=new $c(this,
b)),this.devices.sb16=new D(this,b));a.multiboot&&(a=this.load_multiboot_option_rom(a.multiboot,a.initrd,a.cmdline))&&(this.bios.main?this.option_roms.push(a):this.reg32[0]=this.io.port_read32(244));this.debug_init()};O.prototype.load_multiboot=function(a){this.load_multiboot_option_rom(a,void 0,"")&&(this.reg32[0]=this.io.port_read32(244))};
b),this.devices.vmware=new Hc(this,b),this.devices.uart0=new zc(this,1016,b),a.uart1&&(this.devices.uart1=new zc(this,760,b)),a.uart2&&(this.devices.uart2=new zc(this,1E3,b)),a.uart3&&(this.devices.uart3=new zc(this,744,b)),this.devices.fdc=new V(this,a.fda,a.fdb),c=[[void 0,void 0],[void 0,void 0]],a.hda&&(c[0][0]={buffer:a.hda},c[0][1]={buffer:a.hdb}),c[1][0]={is_cdrom:!0,buffer:a.cdrom},this.devices.ide=new Vc(this,b,c),this.devices.cdrom=this.devices.ide.secondary.master,this.devices.pit=new hb(this,
b),"ne2k"===a.net_device.type?this.devices.net=new Dc(this,b,a.preserve_mac_from_state_image,a.mac_address_translation):"virtio"===a.net_device.type&&(this.devices.virtio_net=new Xc(this,b,a.preserve_mac_from_state_image,a.net_device.mtu)),a.fs9p?this.devices.virtio_9p=new cd(a.fs9p,this,b):a.handle9p?this.devices.virtio_9p=new dd(a.handle9p,this):a.proxy9p&&(this.devices.virtio_9p=new ed(a.proxy9p,this)),a.virtio_console&&(this.devices.virtio_console=new Ec(this,b)),a.virtio_balloon&&(this.devices.virtio_balloon=
new ad(this,b)),this.devices.sb16=new D(this,b));a.multiboot&&(a=this.load_multiboot_option_rom(a.multiboot,a.initrd,a.cmdline))&&(this.bios.main?this.option_roms.push(a):this.reg32[0]=this.io.port_read32(244));this.debug_init()};O.prototype.load_multiboot=function(a){this.load_multiboot_option_rom(a,void 0,"")&&(this.reg32[0]=this.io.port_read32(244))};
O.prototype.load_multiboot_option_rom=function(a,b,c){if(8192>a.byteLength){var d=new Int32Array(2048);(new Uint8Array(d.buffer)).set(new Uint8Array(a))}else d=new Int32Array(a,0,2048);for(var e=0;8192>e;e+=4){if(464367618===d[e>>2]){var f=d[e+4>>2];if(464367618+f+d[e+8>>2]|0)continue}else continue;ua("Multiboot magic found, flags: "+y(f>>>0,8),2);var g=this;this.io.register_read(244,this,function(){return 0},function(){return 0},function(){var n,p=31860;let q=0;if(c){q|=4;g.write32(31760,p);c+="\x00";
var r=(new TextEncoder).encode(c);g.write_blob(r,p);p+=r.length}if(f&2){q|=64;r=0;g.write32(31788,0);g.write32(31792,p);var x=0;var C=!1;for(n=0;4294967296>n;n+=131072)C&&void 0!==g.memory_map_read8[n>>>17]?(g.write32(p,20),g.write32(p+4,x),g.write32(p+8,0),g.write32(p+12,n-x),g.write32(p+16,0),g.write32(p+20,1),p+=24,r+=24,C=!1):C||void 0!==g.memory_map_read8[n>>>17]||(x=n,C=!0);g.write32(31788,r)}x=r=0;if(f&65536){n=d[e+12>>2];r=d[e+16>>2];var t=d[e+20>>2];x=d[e+24>>2];C=d[e+28>>2];y(n,8);y(r,8);
y(t,8);y(x,8);y(C,8);n=new Uint8Array(a,e-(n-r),0===t?void 0:t-r);g.write_blob(n,r);r=C|0;x=Math.max(t,x)}else if(1179403647===d[0]){C=new DataView(a);const [A,M]=Oc(C,Lc);console.assert(52===M);console.assert(1179403647===A.magic,"Bad magic");console.assert(1===A.class,"Unimplemented: 64 bit elf");console.assert(1===A.data,"Unimplemented: big endian");console.assert(1===A.version0,"Bad version0");console.assert(2===A.type,"Unimplemented type");console.assert(1===A.version1,"Bad version1");console.assert(52===
A.ehsize,"Bad header size");console.assert(32===A.phentsize,"Bad program header size");console.assert(40===A.shentsize,"Bad section header size");[r]=Pc(new DataView(C.buffer,C.byteOffset+A.phoff,A.phentsize*A.phnum),Mc,A.phnum);Pc(new DataView(C.buffer,C.byteOffset+A.shoff,A.shentsize*A.shnum),Nc,A.shnum);C=A;n=r;r=C.entry;for(t of n)0!==t.type&&(1===t.type?t.paddr+t.memsz<g.memory_size[0]?(t.filesz&&(n=new Uint8Array(a,t.offset,t.filesz),g.write_blob(n,t.paddr)),x=Math.max(x,t.paddr+t.memsz),r===
y(t,8);y(x,8);y(C,8);n=new Uint8Array(a,e-(n-r),0===t?void 0:t-r);g.write_blob(n,r);r=C|0;x=Math.max(t,x)}else if(1179403647===d[0]){C=new DataView(a);const [A,M]=Pc(C,Mc);console.assert(52===M);console.assert(1179403647===A.magic,"Bad magic");console.assert(1===A.class,"Unimplemented: 64 bit elf");console.assert(1===A.data,"Unimplemented: big endian");console.assert(1===A.version0,"Bad version0");console.assert(2===A.type,"Unimplemented type");console.assert(1===A.version1,"Bad version1");console.assert(52===
A.ehsize,"Bad header size");console.assert(32===A.phentsize,"Bad program header size");console.assert(40===A.shentsize,"Bad section header size");[r]=Qc(new DataView(C.buffer,C.byteOffset+A.phoff,A.phentsize*A.phnum),Nc,A.phnum);Qc(new DataView(C.buffer,C.byteOffset+A.shoff,A.shentsize*A.shnum),Oc,A.shnum);C=A;n=r;r=C.entry;for(t of n)0!==t.type&&(1===t.type?t.paddr+t.memsz<g.memory_size[0]?(t.filesz&&(n=new Uint8Array(a,t.offset,t.filesz),g.write_blob(n,t.paddr)),x=Math.max(x,t.paddr+t.memsz),r===
C.entry&&t.vaddr<=r&&t.vaddr+t.memsz>r&&(r=r-t.vaddr+t.paddr)):y(t.paddr):2===t.type||3===t.type||4===t.type||6===t.type||7===t.type||1685382480===t.type||1685382481===t.type||1685382482===t.type||1685382483===t.type||y(t.type))}b&&(q|=8,g.write32(31764,1),g.write32(31768,p),t=x,0!==(t&4095)&&(t=(t&-4096)+4096),x=t+b.byteLength,g.write32(p,t),g.write32(p+4,x),g.write32(p+8,0),g.write32(p+12,0),g.write_blob(new Uint8Array(b),t));g.write32(31744,q);g.reg32[3]=31744;g.cr[0]=1;g.protected_mode[0]=1;g.flags[0]=
2;g.is_32[0]=1;g.stack_size_32[0]=1;for(p=0;6>p;p++)g.segment_is_null[p]=0,g.segment_offsets[p]=0,g.segment_limits[p]=4294967295,g.sreg[p]=45058;g.instruction_pointer[0]=g.get_seg_cs()+r|0;g.update_state_flags();g.dump_state();g.dump_regs_short();return 732803074});this.io.register_write_consecutive(244,this,function(n){console.log("Test exited with code "+y(n,2));throw"HALT";},function(){},function(){},function(){});for(let n=0;15>=n;n++){function p(q){y(n);y(q,2);q?this.device_raise_irq(n):this.device_lower_irq(n)}
this.io.register_write(8192+n,this,p,p,p)}const l=new Uint8Array(512);(new Uint16Array(l.buffer))[0]=43605;l[2]=1;var h=3;l[h++]=102;l[h++]=229;l[h++]=244;let m=l[h]=0;for(let n=0;n<l.length;n++)m+=l[n];l[h]=-m;return{name:"genroms/multiboot.bin",data:l}}};
@@ -702,22 +709,22 @@ Fc.prototype.lower_irq=function(){this.isr_status=0;this.pci.lower_irq(this.pci_
Y.prototype.get_state=function(){const a=[];a[0]=this.size;a[1]=this.size_supported;a[2]=this.enabled;a[3]=this.notify_offset;a[4]=this.desc_addr;a[5]=this.avail_addr;a[6]=this.avail_last_idx;a[7]=this.used_addr;a[8]=this.num_staged_replies;a[9]=1;return a};
Y.prototype.set_state=function(a){this.size=a[0];this.size_supported=a[1];this.enabled=a[2];this.notify_offset=a[3];this.desc_addr=a[4];this.avail_addr=a[5];this.avail_last_idx=a[6];this.used_addr=a[7];this.num_staged_replies=a[8];this.mask=this.size-1;this.fix_wrapping=1!==a[9]};Y.prototype.reset=function(){this.enabled=!1;this.num_staged_replies=this.used_addr=this.avail_last_idx=this.avail_addr=this.desc_addr=0;this.set_size(this.size_supported)};
Y.prototype.is_configured=function(){return this.desc_addr&&this.avail_addr&&this.used_addr};Y.prototype.enable=function(){this.is_configured();this.enabled=!0};Y.prototype.set_size=function(a){this.size=a;this.mask=a-1};Y.prototype.count_requests=function(){this.fix_wrapping&&(this.fix_wrapping=!1,this.avail_last_idx=(this.avail_get_idx()&~this.mask)+(this.avail_last_idx&this.mask));return this.avail_get_idx()-this.avail_last_idx&65535};Y.prototype.has_request=function(){return 0!==this.count_requests()};
Y.prototype.pop_request=function(){this.has_request();var a=this.avail_get_entry(this.avail_last_idx);a=new ed(this,a);this.avail_last_idx=this.avail_last_idx+1&65535;return a};Y.prototype.push_reply=function(a){const b=this.used_get_idx()+this.num_staged_replies&this.mask;this.used_set_entry(b,a.head_idx,a.length_written);this.num_staged_replies++};
Y.prototype.pop_request=function(){this.has_request();var a=this.avail_get_entry(this.avail_last_idx);a=new fd(this,a);this.avail_last_idx=this.avail_last_idx+1&65535;return a};Y.prototype.push_reply=function(a){const b=this.used_get_idx()+this.num_staged_replies&this.mask;this.used_set_entry(b,a.head_idx,a.length_written);this.num_staged_replies++};
Y.prototype.flush_replies=function(){if(0!==this.num_staged_replies){var a=this.used_get_idx()+this.num_staged_replies&65535;this.used_set_idx(a);this.num_staged_replies=0;this.virtio.is_feature_negotiated(29)?(this.avail_get_used_event(),this.virtio.raise_irq(1)):~this.avail_get_flags()&1&&this.virtio.raise_irq(1)}};Y.prototype.notify_me_after=function(a){a=this.avail_get_idx()+a&65535;this.used_set_avail_event(a)};
Y.prototype.get_descriptor=function(a,b){return{addr_low:this.cpu.read32s(a+16*b),addr_high:this.cpu.read32s(a+16*b+4),len:this.cpu.read32s(a+16*b+8),flags:this.cpu.read16(a+16*b+12),next:this.cpu.read16(a+16*b+14)}};Y.prototype.avail_get_flags=function(){return this.cpu.read16(this.avail_addr)};Y.prototype.avail_get_idx=function(){return this.cpu.read16(this.avail_addr+2)};Y.prototype.avail_get_entry=function(a){return this.cpu.read16(this.avail_addr+4+2*(a&this.mask))};
Y.prototype.avail_get_used_event=function(){return this.cpu.read16(this.avail_addr+4+2*this.size)};Y.prototype.used_get_flags=function(){return this.cpu.read16(this.used_addr)};Y.prototype.used_set_flags=function(a){this.cpu.write16(this.used_addr,a)};Y.prototype.used_get_idx=function(){return this.cpu.read16(this.used_addr+2)};Y.prototype.used_set_idx=function(a){this.cpu.write16(this.used_addr+2,a)};
Y.prototype.used_set_entry=function(a,b,c){this.cpu.write32(this.used_addr+4+8*a,b);this.cpu.write32(this.used_addr+8+8*a,c)};Y.prototype.used_set_avail_event=function(a){this.cpu.write16(this.used_addr+4+8*this.size,a)};
function ed(a,b){this.cpu=a.cpu;this.virtio=a.virtio;this.head_idx=b;this.read_buffers=[];this.length_readable=this.read_buffer_offset=this.read_buffer_idx=0;this.write_buffers=[];this.length_writable=this.length_written=this.write_buffer_offset=this.write_buffer_idx=0;let c=a.desc_addr,d=0,e=a.size,f=!1;const g=this.virtio.is_feature_negotiated(28);do{const h=a.get_descriptor(c,b);y(h.addr_high,8);y(h.addr_low,8);y(h.len,8);y(h.flags,4);y(h.next,4);if(g&&h.flags&4)c=h.addr_low,d=b=0,e=h.len/16;else{if(h.flags&
function fd(a,b){this.cpu=a.cpu;this.virtio=a.virtio;this.head_idx=b;this.read_buffers=[];this.length_readable=this.read_buffer_offset=this.read_buffer_idx=0;this.write_buffers=[];this.length_writable=this.length_written=this.write_buffer_offset=this.write_buffer_idx=0;let c=a.desc_addr,d=0,e=a.size,f=!1;const g=this.virtio.is_feature_negotiated(28);do{const h=a.get_descriptor(c,b);y(h.addr_high,8);y(h.addr_low,8);y(h.len,8);y(h.flags,4);y(h.next,4);if(g&&h.flags&4)c=h.addr_low,d=b=0,e=h.len/16;else{if(h.flags&
2)f=!0,this.write_buffers.push(h),this.length_writable+=h.len;else{if(f)break;this.read_buffers.push(h);this.length_readable+=h.len}d++;if(d>e)break;if(h.flags&1)b=h.next;else break}}while(1)}
ed.prototype.get_next_blob=function(a){let b=0,c=a.length;for(;c&&this.read_buffer_idx!==this.read_buffers.length;){var d=this.read_buffers[this.read_buffer_idx];const e=d.addr_low+this.read_buffer_offset;d=d.len-this.read_buffer_offset;d>c?(d=c,this.read_buffer_offset+=c):(this.read_buffer_idx++,this.read_buffer_offset=0);a.set(this.cpu.read_blob(e,d),b);b+=d;c-=d}return b};
ed.prototype.set_next_blob=function(a){let b=0,c=a.length;for(;c&&this.write_buffer_idx!==this.write_buffers.length;){var d=this.write_buffers[this.write_buffer_idx];const e=d.addr_low+this.write_buffer_offset;d=d.len-this.write_buffer_offset;d>c?(d=c,this.write_buffer_offset+=c):(this.write_buffer_idx++,this.write_buffer_offset=0);this.cpu.write_blob(a.subarray(b,b+d),e);b+=d;c-=d}this.length_written+=b;return b};function fd(a,b,c,d){const e=new Fc(a,{name:"virtio-9p",pci_id:48,device_id:4169,subsystem_device_id:9,common:{initial_port:43008,queues:[{size_supported:32,notify_offset:0}],features:[0,32,29,28],on_driver_ok:()=>{}},notification:{initial_port:43264,single_handler:!1,handlers:[f=>{if(0===f){for(f=e.queues[0];f.has_request();){const g=f.pop_request();d(g)}f.notify_me_after(0)}}]},isr_status:{initial_port:42752},device_specific:{initial_port:42496,struct:[{bytes:2,name:"mount tag length",read:()=>
fd.prototype.get_next_blob=function(a){let b=0,c=a.length;for(;c&&this.read_buffer_idx!==this.read_buffers.length;){var d=this.read_buffers[this.read_buffer_idx];const e=d.addr_low+this.read_buffer_offset;d=d.len-this.read_buffer_offset;d>c?(d=c,this.read_buffer_offset+=c):(this.read_buffer_idx++,this.read_buffer_offset=0);a.set(this.cpu.read_blob(e,d),b);b+=d;c-=d}return b};
fd.prototype.set_next_blob=function(a){let b=0,c=a.length;for(;c&&this.write_buffer_idx!==this.write_buffers.length;){var d=this.write_buffers[this.write_buffer_idx];const e=d.addr_low+this.write_buffer_offset;d=d.len-this.write_buffer_offset;d>c?(d=c,this.write_buffer_offset+=c):(this.write_buffer_idx++,this.write_buffer_offset=0);this.cpu.write_blob(a.subarray(b,b+d),e);b+=d;c-=d}this.length_written+=b;return b};function gd(a,b,c,d){const e=new Fc(a,{name:"virtio-9p",pci_id:48,device_id:4169,subsystem_device_id:9,common:{initial_port:43008,queues:[{size_supported:32,notify_offset:0}],features:[0,32,29,28],on_driver_ok:()=>{}},notification:{initial_port:43264,single_handler:!1,handlers:[f=>{if(0===f){for(f=e.queues[0];f.has_request();){const g=f.pop_request();d(g)}f.notify_me_after(0)}}]},isr_status:{initial_port:42752},device_specific:{initial_port:42496,struct:[{bytes:2,name:"mount tag length",read:()=>
b,write:()=>{}}].concat(Array.from(Array(254).keys()).map(f=>({bytes:1,name:"mount tag name "+f,read:()=>c[f]||0,write:()=>{}})))}});return e}
function bd(a,b,c){this.fs=a;this.bus=c;this.configspace_tagname=[104,111,115,116,57,112];this.configspace_taglen=this.configspace_tagname.length;this.virtio=fd(b,this.configspace_taglen,this.configspace_tagname,this.ReceiveRequest.bind(this));this.virtqueue=this.virtio.queues[0];this.VERSION="9P2000.L";this.msize=this.BLOCKSIZE=8192;this.replybuffer=new Uint8Array(2*this.msize);this.replybuffersize=0;this.fids=[]}
bd.prototype.get_state=function(){var a=[];a[0]=this.configspace_tagname;a[1]=this.configspace_taglen;a[2]=this.virtio;a[3]=this.VERSION;a[4]=this.BLOCKSIZE;a[5]=this.msize;a[6]=this.replybuffer;a[7]=this.replybuffersize;a[8]=this.fids.map(function(b){return[b.inodeid,b.type,b.uid,b.dbg_name]});a[9]=this.fs;return a};
bd.prototype.set_state=function(a){this.configspace_tagname=a[0];this.configspace_taglen=a[1];this.virtio.set_state(a[2]);this.virtqueue=this.virtio.queues[0];this.VERSION=a[3];this.BLOCKSIZE=a[4];this.msize=a[5];this.replybuffer=a[6];this.replybuffersize=a[7];this.fids=a[8].map(function(b){return{inodeid:b[0],type:b[1],uid:b[2],dbg_name:b[3]}});this.fs.set_state(a[9])};bd.prototype.Createfid=function(a,b,c,d){return{inodeid:a,type:b,uid:c,dbg_name:d}};
bd.prototype.update_dbg_name=function(a,b){for(const c of this.fids)c.inodeid===a&&(c.dbg_name=b)};bd.prototype.reset=function(){this.fids=[];this.virtio.reset()};bd.prototype.BuildReply=function(a,b,c){G(["w","b","h"],[c+7,a+1,b],this.replybuffer,0);this.replybuffersize=c+7};bd.prototype.SendError=function(a,b,c){b=G(["w"],[c],this.replybuffer,7);this.BuildReply(6,a,b)};
bd.prototype.SendReply=function(a){a.set_next_blob(this.replybuffer.subarray(0,this.replybuffersize));this.virtqueue.push_reply(a);this.virtqueue.flush_replies()};
bd.prototype.ReceiveRequest=async function(a){var b=new Uint8Array(a.length_readable);a.get_next_blob(b);var c={offset:0},d=I(["w","b","h"],b,c),e=d[1];d=d[2];switch(e){case 8:var f=this.fs.GetTotalSize();var g=this.fs.GetSpace(),h=[16914839];h[1]=this.BLOCKSIZE;h[2]=Math.floor(g/h[1]);h[3]=h[2]-Math.floor(f/h[1]);h[4]=h[2]-Math.floor(f/h[1]);h[5]=this.fs.CountUsedInodes();h[6]=this.fs.CountFreeInodes();h[7]=0;h[8]=256;f=G("wwddddddw".split(""),h,this.replybuffer,7);this.BuildReply(e,d,f);this.SendReply(a);
function cd(a,b,c){this.fs=a;this.bus=c;this.configspace_tagname=[104,111,115,116,57,112];this.configspace_taglen=this.configspace_tagname.length;this.virtio=gd(b,this.configspace_taglen,this.configspace_tagname,this.ReceiveRequest.bind(this));this.virtqueue=this.virtio.queues[0];this.VERSION="9P2000.L";this.msize=this.BLOCKSIZE=8192;this.replybuffer=new Uint8Array(2*this.msize);this.replybuffersize=0;this.fids=[]}
cd.prototype.get_state=function(){var a=[];a[0]=this.configspace_tagname;a[1]=this.configspace_taglen;a[2]=this.virtio;a[3]=this.VERSION;a[4]=this.BLOCKSIZE;a[5]=this.msize;a[6]=this.replybuffer;a[7]=this.replybuffersize;a[8]=this.fids.map(function(b){return[b.inodeid,b.type,b.uid,b.dbg_name]});a[9]=this.fs;return a};
cd.prototype.set_state=function(a){this.configspace_tagname=a[0];this.configspace_taglen=a[1];this.virtio.set_state(a[2]);this.virtqueue=this.virtio.queues[0];this.VERSION=a[3];this.BLOCKSIZE=a[4];this.msize=a[5];this.replybuffer=a[6];this.replybuffersize=a[7];this.fids=a[8].map(function(b){return{inodeid:b[0],type:b[1],uid:b[2],dbg_name:b[3]}});this.fs.set_state(a[9])};cd.prototype.Createfid=function(a,b,c,d){return{inodeid:a,type:b,uid:c,dbg_name:d}};
cd.prototype.update_dbg_name=function(a,b){for(const c of this.fids)c.inodeid===a&&(c.dbg_name=b)};cd.prototype.reset=function(){this.fids=[];this.virtio.reset()};cd.prototype.BuildReply=function(a,b,c){G(["w","b","h"],[c+7,a+1,b],this.replybuffer,0);this.replybuffersize=c+7};cd.prototype.SendError=function(a,b,c){b=G(["w"],[c],this.replybuffer,7);this.BuildReply(6,a,b)};
cd.prototype.SendReply=function(a){a.set_next_blob(this.replybuffer.subarray(0,this.replybuffersize));this.virtqueue.push_reply(a);this.virtqueue.flush_replies()};
cd.prototype.ReceiveRequest=async function(a){var b=new Uint8Array(a.length_readable);a.get_next_blob(b);var c={offset:0},d=I(["w","b","h"],b,c),e=d[1];d=d[2];switch(e){case 8:var f=this.fs.GetTotalSize();var g=this.fs.GetSpace(),h=[16914839];h[1]=this.BLOCKSIZE;h[2]=Math.floor(g/h[1]);h[3]=h[2]-Math.floor(f/h[1]);h[4]=h[2]-Math.floor(f/h[1]);h[5]=this.fs.CountUsedInodes();h[6]=this.fs.CountFreeInodes();h[7]=0;h[8]=256;f=G("wwddddddw".split(""),h,this.replybuffer,7);this.BuildReply(e,d,f);this.SendReply(a);
break;case 112:case 12:h=I(["w","w"],b,c);f=h[0];var l=h[1];b=this.fids[f].inodeid;c=this.fs.GetInode(b);await this.fs.OpenInode(b,l);h=[];h[0]=c.qid;h[1]=this.msize-24;G(["Q","w"],h,this.replybuffer,7);this.BuildReply(e,d,17);this.SendReply(a);break;case 70:h=I(["w","w","s"],b,c);b=h[0];f=h[1];g=h[2];f=this.fs.Link(this.fids[b].inodeid,this.fids[f].inodeid,g);if(0>f){this.SendError(d,-1===f?"Operation not permitted":"Unknown error: "+-f,-f);this.SendReply(a);break}this.BuildReply(e,d,0);this.SendReply(a);
break;case 16:h=I(["w","s","s","w"],b,c);f=h[0];g=h[1];var m=h[3];b=this.fs.CreateSymlink(g,this.fids[f].inodeid,h[2]);c=this.fs.GetInode(b);c.uid=this.fids[f].uid;c.gid=m;G(["Q"],[c.qid],this.replybuffer,7);this.BuildReply(e,d,13);this.SendReply(a);break;case 18:h=I("wswwww".split(""),b,c);f=h[0];g=h[1];l=h[2];b=h[3];c=h[4];m=h[5];b=this.fs.CreateNode(g,this.fids[f].inodeid,b,c);c=this.fs.GetInode(b);c.mode=l;c.uid=this.fids[f].uid;c.gid=m;G(["Q"],[c.qid],this.replybuffer,7);this.BuildReply(e,d,
13);this.SendReply(a);break;case 22:h=I(["w"],b,c);f=h[0];c=this.fs.GetInode(this.fids[f].inodeid);f=G(["s"],[c.symlink],this.replybuffer,7);this.BuildReply(e,d,f);this.SendReply(a);break;case 72:h=I(["w","s","w","w"],b,c);f=h[0];g=h[1];l=h[2];m=h[3];b=this.fs.CreateDirectory(g,this.fids[f].inodeid);c=this.fs.GetInode(b);c.mode=l|16384;c.uid=this.fids[f].uid;c.gid=m;G(["Q"],[c.qid],this.replybuffer,7);this.BuildReply(e,d,13);this.SendReply(a);break;case 14:h=I(["w","s","w","w","w"],b,c);f=h[0];g=
@@ -732,12 +739,12 @@ f){this.SendError(d,"No such file or directory",2);this.SendReply(a);break}f=thi
d,f);this.SendReply(a);break;case 104:h=I(["w","w","s","s","w"],b,c);f=h[0];g=h[4];y(h[1]);this.fids[f]=this.Createfid(0,1,g,"");c=this.fs.GetInode(this.fids[f].inodeid);G(["Q"],[c.qid],this.replybuffer,7);this.BuildReply(e,d,13);this.SendReply(a);this.bus.send("9p-attach");break;case 108:I(["h"],b,c);this.BuildReply(e,d,0);this.SendReply(a);break;case 110:h=I(["w","w","h"],b,c);f=h[0];l=h[1];m=h[2];if(0===m){this.fids[l]=this.Createfid(this.fids[f].inodeid,1,this.fids[f].uid,this.fids[f].dbg_name);
G(["h"],[0],this.replybuffer,7);this.BuildReply(e,d,2);this.SendReply(a);break}g=[];for(h=0;h<m;h++)g.push("s");c=I(g,b,c);b=this.fids[f].inodeid;g=9;var n=0;for(h=0;h<m;h++){b=this.fs.Search(b,c[h]);if(-1===b)break;g+=G(["Q"],[this.fs.GetInode(b).qid],this.replybuffer,g);n++;this.fids[l]=this.Createfid(b,1,this.fids[f].uid,c[h])}G(["h"],[n],this.replybuffer,7);this.BuildReply(e,d,g-7);this.SendReply(a);break;case 120:h=I(["w"],b,c);this.fids[h[0]]&&0<=this.fids[h[0]].inodeid&&(await this.fs.CloseInode(this.fids[h[0]].inodeid),
this.fids[h[0]].inodeid=-1,this.fids[h[0]].type=-1);this.BuildReply(e,d,0);this.SendReply(a);break;case 32:h=I(["w","s","d","w"],b,c);f=h[0];this.fids[f].type=2;this.BuildReply(e,d,0);this.SendReply(a);break;case 30:I(["w","w","s"],b,c),this.SendError(d,"Setxattr not supported",95),this.SendReply(a)}};
function cd(a,b){this.handle_fn=a;this.tag_bufchain=new Map;this.configspace_tagname=[104,111,115,116,57,112];this.configspace_taglen=this.configspace_tagname.length;this.virtio=fd(b,this.configspace_taglen,this.configspace_tagname,async c=>{const d=new Uint8Array(c.length_readable);c.get_next_blob(d);var e=I(["w","b","h"],d,{offset:0})[2];this.tag_bufchain.set(e,c);this.handle_fn(d,f=>{var g=I(["w","b","h"],f,{offset:0})[2];const h=this.tag_bufchain.get(g);h?(h.set_next_blob(f),this.virtqueue.push_reply(h),
this.virtqueue.flush_replies(),this.tag_bufchain.delete(g)):console.error("No bufchain found for tag: "+g)})});this.virtqueue=this.virtio.queues[0]}cd.prototype.get_state=function(){var a=[];a[0]=this.configspace_tagname;a[1]=this.configspace_taglen;a[2]=this.virtio;a[3]=this.tag_bufchain;return a};cd.prototype.set_state=function(a){this.configspace_tagname=a[0];this.configspace_taglen=a[1];this.virtio.set_state(a[2]);this.virtqueue=this.virtio.queues[0];this.tag_bufchain=a[3]};
cd.prototype.reset=function(){this.virtio.reset()};
function dd(a,b){this.socket=void 0;this.cpu=b;this.send_queue=[];this.url=a;this.reconnect_interval=1E4;this.last_connect_attempt=Date.now()-this.reconnect_interval;this.send_queue_limit=64;this.destroyed=!1;this.tag_bufchain=new Map;this.configspace_tagname=[104,111,115,116,57,112];this.configspace_taglen=this.configspace_tagname.length;this.virtio=fd(b,this.configspace_taglen,this.configspace_tagname,async c=>{const d=new Uint8Array(c.length_readable);c.get_next_blob(d);const e=I(["w","b","h"],
d,{offset:0})[2];this.tag_bufchain.set(e,c);this.send(d)});this.virtqueue=this.virtio.queues[0]}dd.prototype.get_state=function(){var a=[];a[0]=this.configspace_tagname;a[1]=this.configspace_taglen;a[2]=this.virtio;a[3]=this.tag_bufchain;return a};dd.prototype.set_state=function(a){this.configspace_tagname=a[0];this.configspace_taglen=a[1];this.virtio.set_state(a[2]);this.virtqueue=this.virtio.queues[0];this.tag_bufchain=a[3]};dd.prototype.reset=function(){this.virtio.reset()};
dd.prototype.handle_message=function(a){a=new Uint8Array(a.data);const b=I(["w","b","h"],a,{offset:0})[2],c=this.tag_bufchain.get(b);c?(c.set_next_blob(a),this.virtqueue.push_reply(c),this.virtqueue.flush_replies(),this.tag_bufchain.delete(b)):console.error("Virtio9pProxy: No bufchain found for tag: "+b)};dd.prototype.handle_close=function(){this.destroyed||(this.connect(),setTimeout(this.connect.bind(this),this.reconnect_interval))};
dd.prototype.handle_open=function(){for(var a=0;a<this.send_queue.length;a++)this.send(this.send_queue[a]);this.send_queue=[]};dd.prototype.handle_error=function(){};dd.prototype.destroy=function(){this.destroyed=!0;this.socket&&this.socket.close()};
dd.prototype.connect=function(){if("undefined"!==typeof WebSocket){if(this.socket){var a=this.socket.readyState;if(0===a||1===a)return}a=Date.now();if(!(this.last_connect_attempt+this.reconnect_interval>a)){this.last_connect_attempt=Date.now();try{this.socket=new WebSocket(this.url)}catch(b){console.error(b);return}this.socket.binaryType="arraybuffer";this.socket.onopen=this.handle_open.bind(this);this.socket.onmessage=this.handle_message.bind(this);this.socket.onclose=this.handle_close.bind(this);
this.socket.onerror=this.handle_error.bind(this)}}};dd.prototype.send=function(a){this.socket&&1===this.socket.readyState?this.socket.send(a):(this.send_queue.push(a),this.send_queue.length>2*this.send_queue_limit&&(this.send_queue=this.send_queue.slice(-this.send_queue_limit)),this.connect())};dd.prototype.change_proxy=function(a){this.url=a;this.socket&&(this.socket.onclose=function(){},this.socket.onerror=function(){},this.socket.close(),this.socket=void 0)};}).call(this);
function dd(a,b){this.handle_fn=a;this.tag_bufchain=new Map;this.configspace_tagname=[104,111,115,116,57,112];this.configspace_taglen=this.configspace_tagname.length;this.virtio=gd(b,this.configspace_taglen,this.configspace_tagname,async c=>{const d=new Uint8Array(c.length_readable);c.get_next_blob(d);var e=I(["w","b","h"],d,{offset:0})[2];this.tag_bufchain.set(e,c);this.handle_fn(d,f=>{var g=I(["w","b","h"],f,{offset:0})[2];const h=this.tag_bufchain.get(g);h?(h.set_next_blob(f),this.virtqueue.push_reply(h),
this.virtqueue.flush_replies(),this.tag_bufchain.delete(g)):console.error("No bufchain found for tag: "+g)})});this.virtqueue=this.virtio.queues[0]}dd.prototype.get_state=function(){var a=[];a[0]=this.configspace_tagname;a[1]=this.configspace_taglen;a[2]=this.virtio;a[3]=this.tag_bufchain;return a};dd.prototype.set_state=function(a){this.configspace_tagname=a[0];this.configspace_taglen=a[1];this.virtio.set_state(a[2]);this.virtqueue=this.virtio.queues[0];this.tag_bufchain=a[3]};
dd.prototype.reset=function(){this.virtio.reset()};
function ed(a,b){this.socket=void 0;this.cpu=b;this.send_queue=[];this.url=a;this.reconnect_interval=1E4;this.last_connect_attempt=Date.now()-this.reconnect_interval;this.send_queue_limit=64;this.destroyed=!1;this.tag_bufchain=new Map;this.configspace_tagname=[104,111,115,116,57,112];this.configspace_taglen=this.configspace_tagname.length;this.virtio=gd(b,this.configspace_taglen,this.configspace_tagname,async c=>{const d=new Uint8Array(c.length_readable);c.get_next_blob(d);const e=I(["w","b","h"],
d,{offset:0})[2];this.tag_bufchain.set(e,c);this.send(d)});this.virtqueue=this.virtio.queues[0]}ed.prototype.get_state=function(){var a=[];a[0]=this.configspace_tagname;a[1]=this.configspace_taglen;a[2]=this.virtio;a[3]=this.tag_bufchain;return a};ed.prototype.set_state=function(a){this.configspace_tagname=a[0];this.configspace_taglen=a[1];this.virtio.set_state(a[2]);this.virtqueue=this.virtio.queues[0];this.tag_bufchain=a[3]};ed.prototype.reset=function(){this.virtio.reset()};
ed.prototype.handle_message=function(a){a=new Uint8Array(a.data);const b=I(["w","b","h"],a,{offset:0})[2],c=this.tag_bufchain.get(b);c?(c.set_next_blob(a),this.virtqueue.push_reply(c),this.virtqueue.flush_replies(),this.tag_bufchain.delete(b)):console.error("Virtio9pProxy: No bufchain found for tag: "+b)};ed.prototype.handle_close=function(){this.destroyed||(this.connect(),setTimeout(this.connect.bind(this),this.reconnect_interval))};
ed.prototype.handle_open=function(){for(var a=0;a<this.send_queue.length;a++)this.send(this.send_queue[a]);this.send_queue=[]};ed.prototype.handle_error=function(){};ed.prototype.destroy=function(){this.destroyed=!0;this.socket&&this.socket.close()};
ed.prototype.connect=function(){if("undefined"!==typeof WebSocket){if(this.socket){var a=this.socket.readyState;if(0===a||1===a)return}a=Date.now();if(!(this.last_connect_attempt+this.reconnect_interval>a)){this.last_connect_attempt=Date.now();try{this.socket=new WebSocket(this.url)}catch(b){console.error(b);return}this.socket.binaryType="arraybuffer";this.socket.onopen=this.handle_open.bind(this);this.socket.onmessage=this.handle_message.bind(this);this.socket.onclose=this.handle_close.bind(this);
this.socket.onerror=this.handle_error.bind(this)}}};ed.prototype.send=function(a){this.socket&&1===this.socket.readyState?this.socket.send(a):(this.send_queue.push(a),this.send_queue.length>2*this.send_queue_limit&&(this.send_queue=this.send_queue.slice(-this.send_queue_limit)),this.connect())};ed.prototype.change_proxy=function(a){this.url=a;this.socket&&(this.socket.onclose=function(){},this.socket.onerror=function(){},this.socket.close(),this.socket=void 0)};}).call(this);

View File

@@ -0,0 +1,81 @@
// With dns_method:"doh" the guest's UDP/53 queries are POSTed verbatim to
// cloudflare-dns.com/dns-query. Real DNS can't answer the app's magic names
// (single-label "windows95", "<port>.external"), which the static resolver
// used to map to a placeholder IP so the fetch adapter could take over via
// the Host header. This shim wraps global fetch, spots DoH requests for
// those names, and answers them locally with the same placeholder; every
// other lookup goes out to Cloudflare unchanged.
const PLACEHOLDER_IP = [192, 168, 87, 1];
function qnameOf(msg: Uint8Array): string {
const labels: string[] = [];
let i = 12;
while (i < msg.length) {
const len = msg[i++];
if (len === 0) break;
labels.push(String.fromCharCode(...msg.subarray(i, i + len)));
i += len;
}
return labels.join(".");
}
function isMagicName(name: string): boolean {
if (!name.includes(".")) return true; // windows95, host, …
if (/^\d+\.external$/i.test(name)) return true; // 8080.external → localhost:8080
return false;
}
function synthAResponse(query: Uint8Array): Uint8Array {
// End of the question section: QNAME (null-terminated) + QTYPE(2) + QCLASS(2).
let i = 12;
while (query[i] !== 0) i += query[i] + 1;
const qend = i + 1 + 4;
const out = new Uint8Array(qend + 16);
out.set(query.subarray(0, qend));
out[2] = 0x81;
out[3] = 0x80; // QR=1 RD=1 RA=1, RCODE=0
out[4] = 0;
out[5] = 1; // QDCOUNT=1
out[6] = 0;
out[7] = 1; // ANCOUNT=1
out[8] = out[9] = out[10] = out[11] = 0;
let o = qend;
out[o++] = 0xc0;
out[o++] = 0x0c; // NAME → pointer to question
out[o++] = 0;
out[o++] = 1; // TYPE A
out[o++] = 0;
out[o++] = 1; // CLASS IN
out[o++] = 0;
out[o++] = 0;
out[o++] = 0x02;
out[o++] = 0x58; // TTL 600
out[o++] = 0;
out[o++] = 4; // RDLENGTH
out.set(PLACEHOLDER_IP, o);
return out;
}
export function setupDnsShim() {
const realFetch = globalThis.fetch;
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url.includes("/dns-query") && init?.body instanceof Uint8Array) {
const q = init.body as Uint8Array;
const name = qnameOf(q).toLowerCase();
if (isMagicName(name)) {
const body = synthAResponse(q);
return Promise.resolve(
new Response(body as unknown as BodyInit, {
status: 200,
headers: { "content-type": "application/dns-message" },
}),
);
}
}
return realFetch(input, init);
}) as typeof fetch;
}

View File

@@ -0,0 +1,153 @@
// Bridge v86's userspace TCP stack to real host sockets so the guest can
// reach arbitrary TCP ports — not just the fetch adapter's port-80 HTTP path.
// We keep relay_url:"fetch" (for DHCP/ARP/ICMP/DNS plumbing and the existing
// port-80 handling) and simply claim every other SYN on the same bus event.
//
// Requires net_device.dns_method:"doh" so the guest resolves real IPs; the
// default "static" mode hands out 192.168.87.1 for everything, which only
// works because the fetch adapter re-reads the Host header.
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as net from "net";
interface TCPConnection {
sport: number; // dest port the guest connected to
psrc: Uint8Array; // dest IP (4 bytes) — "source" from the router's POV
tuple: string;
net: { tcp_conn: Record<string, unknown> };
on(event: "data", handler: (data: Uint8Array) => void): void;
write(data: Uint8Array): void;
accept(): void;
close(): void;
on_shutdown: () => void;
on_close: () => void;
}
interface V86 {
bus: { register(name: string, fn: (arg: unknown) => void): void };
}
const LOG_FILE =
process.env.WIN95_TCP_RELAY_LOG ||
path.join(os.tmpdir(), "win95-tcp-relay.log");
try {
fs.writeFileSync(LOG_FILE, `--- ${new Date().toISOString()} ---\n`);
} catch {}
const log = (...a: unknown[]) => {
console.log("[tcp-relay]", ...a);
try {
fs.appendFileSync(LOG_FILE, a.join(" ") + "\n");
} catch {}
};
// Ports already claimed by other in-process handlers.
const RESERVED_PORTS = new Set([80, 139]);
// WIN95_TCP_TEST_PORT=<n> short-circuits that port to an in-process fake
// upstream that writes a banner asynchronously (i.e., from outside a CPU
// tick) and then echoes everything back. Lets the probe harness exercise
// the recv() path deterministically without a real network endpoint.
const TEST_PORT = Number(process.env.WIN95_TCP_TEST_PORT) || 0;
const TEST_BANNER_BYTES = Number(process.env.WIN95_TCP_TEST_BYTES) || 3000;
// Destinations we never relay to. The emulated LAN (192.168.86/87) has nothing
// real behind it; loopback and link-local would let guest software poke at the
// host's own services or cloud-metadata endpoints. The rest of RFC1918 is left
// reachable on purpose — talking to a NAS or BBS on the user's LAN is a
// legitimate use of this bridge, and the port-80 fetch path already allows it.
function isBlockedDest(p: Uint8Array): boolean {
const [a, b] = p;
if (a === 192 && b === 168 && (p[2] === 86 || p[2] === 87)) return true;
if (a === 127 || a === 0 || a >= 224) return true; // loopback, "this", multicast+
if (a === 169 && b === 254) return true; // link-local / metadata
return false;
}
export function setupTcpRelay(emulator: V86) {
emulator.bus.register("tcp-connection", (c: unknown) => {
const conn = c as TCPConnection;
const port = conn.sport;
if (RESERVED_PORTS.has(port)) return;
if (isBlockedDest(conn.psrc)) return;
const ip = Array.from(conn.psrc).join(".");
// Must accept synchronously or v86 RSTs the SYN right after dispatch.
conn.accept();
log(`${ip}:${port} (${conn.tuple})`);
if (TEST_PORT && port === TEST_PORT) {
const mode = process.env.WIN95_TCP_TEST_MODE || "banner";
const banner = Buffer.alloc(TEST_BANNER_BYTES, 0x41);
banner.write(`HELLO from tcp-relay test, ${TEST_BANNER_BYTES}B\r\n`);
let n = 0;
conn.on("data", (d) => {
if (d.length === 0) return;
n += d.length;
log(`test ← guest ${d.length}B (total ${n})`);
if (mode === "after-data" && n >= 1) {
setTimeout(() => {
log(`test → guest ${banner.length}B banner (async, after-data)`);
conn.write(banner);
}, 10);
}
});
if (mode === "banner")
setTimeout(
() => {
log(`test → guest ${banner.length}B banner (async)`);
conn.write(banner);
},
Number(process.env.WIN95_TCP_TEST_DELAY) || 50,
);
conn.on_shutdown = conn.on_close = () => log("test guest closed");
return;
}
let connected = false;
let pending: Buffer[] | null = [];
let upstreamGone = false;
const sock = net.connect({ host: ip, port });
sock.on("connect", () => {
connected = true;
if (pending) {
for (const b of pending) sock.write(b);
pending = null;
}
});
sock.on("data", (d) => conn.write(d));
sock.on("close", () => {
upstreamGone = true;
conn.close();
});
sock.on("error", (e: NodeJS.ErrnoException) => {
log(`${ip}:${port} ${e.code || e.message}`);
upstreamGone = true;
conn.close();
});
// tcp_data is a subarray into v86's reused frame buffer — copy before
// handing to an async writer.
conn.on("data", (d) => {
if (d.length === 0) return;
const buf = Buffer.from(d);
if (connected) sock.write(buf);
else pending?.push(buf);
});
const teardown = () => {
if (upstreamGone) return;
upstreamGone = true;
if (connected) sock.end();
else sock.destroy();
};
conn.on_shutdown = teardown;
conn.on_close = teardown;
});
log("active — guest TCP to non-80/139 ports is bridged to host sockets");
}

View File

@@ -0,0 +1,118 @@
// Deep packet trace for the v86 userspace TCP ↔ NE2000 path. Activated by
// WIN95_TCP_TRACE=<port> — every guest-TX and host-RX TCP frame on that
// dest port is decoded and appended to /tmp/win95-tcp-trace.log, and the
// NE2000 receive() is wrapped so we can see whether frames injected from
// outside a CPU tick ever land in the RX ring (vs. being filtered out).
import * as fs from "fs";
const TRACE_FILE =
process.env.WIN95_TCP_TRACE_FILE || "/tmp/win95-tcp-trace.log";
interface V86 {
bus: { register(name: string, fn: (arg: unknown) => void): void };
v86?: { cpu?: { devices?: { net?: unknown } } };
}
function flags(b: number): string {
let s = "";
if (b & 1) s += "F";
if (b & 2) s += "S";
if (b & 4) s += "R";
if (b & 8) s += "P";
if (b & 16) s += "A";
return s || ".";
}
function decodeTcp(f: Uint8Array, port: number) {
if (f.length < 54) return null;
if (((f[12] << 8) | f[13]) !== 0x0800) return null;
const ihl = (f[14] & 0x0f) * 4;
if (f[14 + 9] !== 6) return null;
const ipLen = (f[16] << 8) | f[17];
const t = 14 + ihl;
const sport = (f[t] << 8) | f[t + 1];
const dport = (f[t + 2] << 8) | f[t + 3];
if (sport !== port && dport !== port) return null;
const seq =
(f[t + 4] * 0x1000000 + (f[t + 5] << 16) + (f[t + 6] << 8) + f[t + 7]) >>>
0;
const ack =
(f[t + 8] * 0x1000000 + (f[t + 9] << 16) + (f[t + 10] << 8) + f[t + 11]) >>>
0;
const doff = (f[t + 12] >> 4) * 4;
const fl = f[t + 13];
const win = (f[t + 14] << 8) | f[t + 15];
const dlen = ipLen - ihl - doff;
return { sport, dport, seq, ack, fl: flags(fl), win, dlen, frameLen: f.length };
}
export function setupTcpTrace(emulator: V86) {
const port = Number(process.env.WIN95_TCP_TRACE);
if (!port) return;
const t0 = Date.now();
fs.writeFileSync(TRACE_FILE, `--- trace port ${port} ---\n`);
const w = (s: string) => {
const line = `[${((Date.now() - t0) / 1000).toFixed(3)}s] ${s}\n`;
try {
fs.appendFileSync(TRACE_FILE, line);
} catch {}
};
emulator.bus.register("net0-send", (raw: unknown) => {
const f = raw as Uint8Array;
const r = decodeTcp(f, port);
if (r)
w(
`guest→ ${r.sport}>${r.dport} seq=${r.seq} ack=${r.ack} ${r.fl} win=${r.win} len=${r.dlen}`,
);
else if (f.length >= 14) {
const et = (f[12] << 8) | f[13];
const dst =
et === 0x0800 ? f.subarray(30, 34).join(".") : et === 0x0806 ? "arp" : "?";
w(
`guest→ [other] len=${f.length}@${f.byteOffset} et=0x${et.toString(16)} dst=${dst}`,
);
}
});
// Wrap NE2000 receive() to see if/why a frame is dropped at the NIC layer.
const arm = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ne2k = (emulator as any).v86?.cpu?.devices?.net;
if (!ne2k) return false;
const orig = ne2k.receive.bind(ne2k);
ne2k.receive = function (a: Uint8Array) {
const r = decodeTcp(a, port);
if (r) {
const pages = 1 + ((Math.max(60, a.length) + 4) >> 8);
const avail =
this.boundary > this.curpg
? this.boundary - this.curpg
: this.pstop - this.curpg + this.boundary - this.pstart;
const macOk =
a[0] === this.mac[0] && a[1] === this.mac[1] && a[2] === this.mac[2];
const drop =
this.cr & 1
? "STP"
: !macOk && a[0] !== 0xff && !(this.rxcr & 16)
? "MAC"
: avail < pages && this.boundary !== 0
? "FULL"
: "";
w(
` ne2k.recv frame=${a.length} sip=${a.slice(26, 30).join(".")} cr=0x${this.cr.toString(16)}` +
` cur=${this.curpg} bnd=${this.boundary}${drop ? " DROP:" + drop : ""}`,
);
}
return orig(a);
};
w("ne2k.receive wrapped");
return true;
};
if (!arm()) {
const p = setInterval(() => arm() && clearInterval(p), 100);
setTimeout(() => clearInterval(p), 10000);
}
}

View File

@@ -16,12 +16,24 @@ a host folder as a network drive. Read-only. ~1500 lines.
## Protocol gotchas (learned the hard way)
### NEGOTIATE: don't pick NT LM 0.12 unless you implement the NT response
### NEGOTIATE: NT LM 0.12 is the only path to long filenames
Win95 offers `["PC NETWORK PROGRAM 1.0", "MICROSOFT NETWORKS 3.0", "DOS LM1.2X002",
"DOS LANMAN2.1", "Windows for Workgroups 3.1a", "NT LM 0.12"]`. We send the
13-word LANMAN-style negotiate response. If you pick `NT LM 0.12` and send 13
words, Win95 silently drops the connection — it expects the 17-word NT response
with capability flags. Pick `DOS LANMAN2.1` instead.
"DOS LANMAN2.1", "Windows for Workgroups 3.1a", "NT LM 0.12"]`. We pick
`NT LM 0.12` and send the 17-word NT response (Capabilities=0 — no UNICODE, no
NT_STATUS, no NT_FIND, so the rest of the protocol stays OEM/DOS-error). On any
LANMAN dialect Win95's redirector lists directories via `CMD_SEARCH` (0x81) whose
13-byte name field hard-caps at 8.3; under NT LM 0.12 it switches to
`TRANS2/FIND_FIRST2` and asks for level `0x104` (FILE_BOTH_DIRECTORY_INFO)
**regardless** of CAP_NT_FIND. We implement that level — the 94-byte fixed prefix
plus OEM long name, ShortName always UTF-16LE per spec. The 13-word LANMAN
response is kept as a fallback for clients that don't offer NT.
### Shares
Two disk shares plus IPC$. The user share is named after `path.basename()` of the
mounted folder (sanitized, ≤12 chars). `TOOLS` is purely synthetic — `_MAPZ.BAT`,
`README.TXT` — so the user's listing isn't cluttered. `treeConnect` routes by
share name to a TID; every path-resolving handler branches on TID so the TOOLS
tree never touches the host fs.
### SEARCH (0x81): single-file probes vs wildcard listings
`SEARCH "\FOO.TXT"` is a stat probe — Win95 wants exactly one entry back. If you
@@ -85,7 +97,7 @@ Clean API. The new code keeps both paths; the bus event is a no-op on old builds
- Share path validated in main-process IPC (`realpathSync` + `isDirectory()`).
## Tests
`test-standalone.ts`35 protocol tests, full round-trips with real file I/O.
Run: `npx tsc --ignoreConfig --module commonjs --target es2020 --esModuleInterop
--moduleResolution bundler --outDir /tmp/smb-test --skipLibCheck
src/renderer/smb/*.ts && node /tmp/smb-test/test-standalone.js`
`test-standalone.ts`55 protocol tests, full round-trips with real file I/O.
Run: `npx ts-node --skip-project --transpile-only --compiler-options
'{"module":"commonjs","moduleResolution":"bundler","ignoreDeprecations":"6.0"}'
src/renderer/smb/test-standalone.ts`

View File

@@ -12,10 +12,10 @@ import * as os from "os";
import * as path from "path";
import { NetBIOSFramer, nbPositiveResponse, nbWrap } from "./netbios";
import { setupNbns } from "./nbns";
import { SmbSession } from "./server";
import { SmbSession, shareNameFor, TOOLS_SHARE } from "./server";
// SPIKE diagnostics: tee everything to a file so we can debug without DevTools
const LOG_FILE = path.join(os.tmpdir(), "windows95-smb.log");
const LOG_FILE = process.env.WIN95_SMB_LOG || path.join(os.tmpdir(), "windows95-smb.log");
try { fs.writeFileSync(LOG_FILE, `--- ${new Date().toISOString()} ---\n`); } catch {}
const origLog = console.log;
console.log = (...args: unknown[]) => {
@@ -56,8 +56,9 @@ interface V86 {
const log = (...a: unknown[]) => console.log("[smb]", ...a);
export function setupSmbShare(emulator: V86, hostPath: string) {
log(`serving ${hostPath} on \\\\HOST\\host (port 139)`);
export function setupSmbShare(emulator: V86, hostPath: string, toolsRoot?: string) {
log(`serving ${hostPath} on \\\\HOST\\${shareNameFor(hostPath)} ` +
`(+ \\\\HOST\\${TOOLS_SHARE}${toolsRoot ? `${toolsRoot}` : ""}) port 139`);
// SPIKE diagnostic: count every ethernet frame so we know if the NIC is
// emitting anything at all (DHCP, ARP, anything). Logged on a timer so
@@ -100,7 +101,7 @@ export function setupSmbShare(emulator: V86, hostPath: string) {
const wireConn = (conn: TCPConnection) => {
log(`← TCP SYN ${conn.tuple}`);
const framer = new NetBIOSFramer();
const session = new SmbSession(hostPath);
const session = new SmbSession(hostPath, toolsRoot);
const handler = (data: Uint8Array) => {
for (const msg of framer.push(data)) {

View File

@@ -8,9 +8,9 @@ import {
parseSmb, buildSmb, dosError, andxNone, cmdName, SmbHeader,
CMD_NEGOTIATE, CMD_SESSION_SETUP_ANDX, CMD_TREE_CONNECT_ANDX,
CMD_TREE_DISCONNECT, CMD_LOGOFF_ANDX, CMD_NT_CREATE_ANDX, CMD_OPEN_ANDX,
CMD_READ_ANDX, CMD_CLOSE, CMD_TRANSACTION, CMD_TRANSACTION2, CMD_ECHO,
CMD_READ, CMD_READ_RAW, CMD_READ_ANDX, CMD_SEEK, CMD_CLOSE, CMD_TRANSACTION, CMD_TRANSACTION2, CMD_ECHO,
CMD_QUERY_INFORMATION, CMD_FIND_CLOSE2, CMD_CHECK_DIRECTORY, CMD_SEARCH,
TRANS2_FIND_FIRST2, TRANS2_FIND_NEXT2, TRANS2_QUERY_PATH_INFO,
TRANS2_FIND_FIRST2, TRANS2_FIND_NEXT2, TRANS2_QUERY_FS_INFO, TRANS2_QUERY_PATH_INFO,
ERRDOS, ERRSRV, ERR_BADFILE, ERR_BADPATH, ERR_BADFID, ERR_NOFILES, ERR_BADFUNC,
} from "./smb";
@@ -19,17 +19,34 @@ const hex = (b: Uint8Array, n = 64) =>
Array.from(b.slice(0, n)).map(x => x.toString(16).padStart(2, "0")).join(" ") +
(b.length > n ? ` …(+${b.length - n})` : "");
/**
* Derive the share name shown under \\HOST from the mounted directory's
* basename. NetShareEnum's B13 field caps it at 12 chars + null; SMB share
* names are case-insensitive OEM, so uppercase and strip to LANMAN-safe chars.
*/
export function shareNameFor(rootPath: string): string {
const base = path.basename(rootPath)
.replace(/[^A-Za-z0-9_$~!#%&'()@^`{}.-]/g, "")
.toUpperCase()
.slice(0, 12);
// A folder literally named "Tools" or "ipc$" would shadow a reserved share
// and become unreachable — fall back to the generic name instead.
return (!base || base === TOOLS_SHARE || base === "IPC$") ? "HOST" : base;
}
interface OpenFile {
hostPath: string;
fd: number;
size: number;
isDir: boolean;
pos?: number;
virtual?: Uint8Array;
}
interface DirEntry {
name: string; // real filename (long)
name: string; // display name (long, sanitized for Win95)
sfn: string; // 8.3 name shown to the client
attr: number; // DOS attribute word
stat: { isDirectory(): boolean; size: number; mtime: Date };
}
@@ -38,14 +55,43 @@ interface SearchState {
idx: number;
}
const ATTR_HIDDEN = 0x02;
const ATTR_SYSTEM = 0x04;
const ATTR_DIRECTORY = 0x10;
const ATTR_ARCHIVE = 0x20;
// Host-side junk that shouldn't clutter the guest's view. Marked H+S so
// Explorer hides it by default but "View → Show all files" still works.
const SYSTEM_JUNK = new Set([
".ds_store", ".localized", ".trashes", ".fseventsd", ".spotlight-v100",
".documentrevisions-v100", ".temporaryitems", ".volumeicon.icns",
"desktop.ini", "thumbs.db", "ehthumbs.db",
]);
function hostAttrs(realName: string, isDir: boolean): number {
let a = isDir ? ATTR_DIRECTORY : ATTR_ARCHIVE;
if (SYSTEM_JUNK.has(realName.toLowerCase())) a |= ATTR_HIDDEN | ATTR_SYSTEM;
else if (realName.startsWith(".")) a |= ATTR_HIDDEN;
return a;
}
// Tree IDs. TID routes every file op: SHARE = the user's mounted folder,
// TOOLS = a purely synthetic share holding _MAPZ.BAT and friends so they
// don't clutter the user's directory listing.
const TID_SHARE = 1;
const TID_TOOLS = 2;
const TID_IPC = 0xfffe;
export const TOOLS_SHARE = "TOOLS";
const NT_DIALECT = "NT LM 0.12";
const DIALECTS = [
// Our negotiate response uses the 13-word LANMAN format, so we MUST NOT
// pick "NT LM 0.12" — Win95 expects the 17-word NT response for that
// dialect and will silently drop a malformed reply. Win95's actual
// dialect strings have a "DOS " prefix (vs the bare names NT/2000 send).
// Prefer NT LM 0.12 — it's the only dialect where Win95's redirector
// switches to TRANS2/FIND_FIRST2 (long filenames). On LANMAN it falls back
// to CMD_SEARCH whose 13-byte name field hard-caps us at 8.3. The two
// dialects have different negotiate-response shapes (17-word NT vs 13-word
// LM), branched on below. Win95's strings have a "DOS " prefix vs the bare
// names NT/2000 send.
NT_DIALECT,
"DOS LANMAN2.1",
"LANMAN2.1",
"Windows for Workgroups 3.1a",
@@ -64,34 +110,81 @@ export class SmbSession {
// consult it so clicking "15UNDE~2.PDF" finds the right long-named file.
private sfnMaps = new Map<string, Map<string, string>>();
private readonly realRoot: string;
public capture = true;
private readonly toolsRoot?: string;
public readonly shareName: string;
public capture = !!process.env.WIN95_SMB_CAPTURE;
// Synthetic files served at the share root. They show up in directory
// listings and OPEN/READ work, but they don't exist on the host fs —
// just in-memory bytes. _MAPZ.BAT maps the share to Z: when the user
// double-clicks it; copying it to C:\WINDOWS\Start Menu\Programs\StartUp
// makes the mapping survive reboots.
private readonly virtuals = new Map<string, Uint8Array>([
["_MAPZ.BAT", new TextEncoder().encode(
"@ECHO OFF\r\n" +
"NET USE Z: \\\\HOST\\HOST\r\n" +
"ECHO Share mapped to Z:\r\n" +
"ECHO Copy this file to C:\\WINDOWS\\STARTM~1\\PROGRAMS\\STARTUP\r\n" +
"ECHO to reconnect automatically on every boot.\r\n" +
"PAUSE\r\n"
)],
]);
private readonly virtuals: Map<string, Uint8Array>;
constructor(rootPath: string) {
constructor(rootPath: string, toolsRoot?: string) {
this.realRoot = fs.realpathSync(rootPath);
this.shareName = shareNameFor(this.realRoot);
const enc = (s: string) => new TextEncoder().encode(s);
this.virtuals = new Map([
["README.TXT", enc(
"windows95 tools\r\n" +
"----------------\r\n" +
"These files are served by the windows95 app from\r\n" +
` ${toolsRoot ?? "(in-memory)"}\r\n\r\n` +
` \\\\HOST\\${this.shareName.padEnd(12)} your shared folder (${this.realRoot})\r\n` +
` \\\\HOST\\${TOOLS_SHARE.padEnd(12)} this folder\r\n\r\n` +
"_MAPZ.BAT maps your shared folder to drive Z:. Copy it to\r\n" +
" C:\\WINDOWS\\Start Menu\\Programs\\StartUp to reconnect\r\n" +
" on every boot.\r\n"
)],
["_MAPZ.BAT", enc(
"@ECHO OFF\r\n" +
`NET USE Z: \\\\HOST\\${this.shareName}\r\n` +
"ECHO Share mapped to Z:\r\n" +
"ECHO Copy this file to C:\\WINDOWS\\STARTM~1\\PROGRAMS\\STARTUP\r\n" +
"ECHO to reconnect automatically on every boot.\r\n" +
"PAUSE\r\n"
)],
]);
this.toolsRoot = toolsRoot && fs.existsSync(toolsRoot)
? fs.realpathSync(toolsRoot)
: undefined;
}
private getVirtual(smbPath: string): Uint8Array | undefined {
private getVirtual(tid: number, smbPath: string): Uint8Array | undefined {
if (tid !== TID_TOOLS) return undefined;
const p = smbPath.replace(/^[\\\/]+/, "").replace(/\\/g, "/");
if (p.includes("/")) return undefined; // root-only
return this.virtuals.get(p.toUpperCase());
}
private listTools(): DirEntry[] {
const now = new Date();
const stat = (size: number) => ({ isDirectory: () => false, size, mtime: now });
return Array.from(this.virtuals, ([name, bytes]) =>
({ name, sfn: name, attr: ATTR_ARCHIVE, stat: stat(bytes.length) }));
}
/**
* Directory listing for SEARCH/FIND_FIRST2. Overlays the synthetic
* README/_MAPZ at the TOOLS root; everything else hits the backing fs.
* Returns null when the directory doesn't resolve (caller emits BADPATH).
*/
private listForSearch(tid: number, dirPart: string):
{ all: DirEntry[]; dotStat: DirEntry["stat"] } | null {
const hostDir = this.resolve(tid, dirPart || "\\");
const hostOk = !!hostDir && fs.existsSync(hostDir);
if (tid === TID_TOOLS && isRootPath(dirPart)) {
return {
all: hostOk ? [...this.listTools(), ...this.listDir(hostDir!)] : this.listTools(),
dotStat: hostOk ? fs.statSync(hostDir!)
: { isDirectory: () => true, size: 0, mtime: new Date() },
};
}
if (!hostOk) return null;
return { all: this.listDir(hostDir!), dotStat: fs.statSync(hostDir!) };
}
/**
* Read a host directory once, generate stable 8.3 names, cache the mapping.
* The cache lives for the session — directory contents changing underneath
@@ -104,22 +197,21 @@ export class SmbSession {
this.sfnMaps.set(hostDir, sfnMap);
const entries: DirEntry[] = [];
const aliases: [string, string][] = [];
for (const [sfn, real] of sfnMap) {
try {
entries.push({ name: real, sfn, stat: fs.statSync(path.join(hostDir, real)) });
// The long name we send is single-byte OEM. Anything outside that
// (emoji, CJK) truncates to its low byte, which can land on a
// Windows-illegal char and wedge Explorer's icon renderer. Sanitize
// for display and add the sanitized form to the lookup map so OPEN
// on the displayed name still finds the real file.
const name = displayName(real);
if (name !== real) aliases.push([name.toUpperCase(), real]);
const stat = fs.statSync(path.join(hostDir, real));
entries.push({ name, sfn, attr: hostAttrs(real, stat.isDirectory()), stat });
} catch { /* raced — skip */ }
}
// Virtuals only at root. They're already 8.3.
if (hostDir === this.realRoot) {
const now = new Date();
for (const [name, bytes] of this.virtuals) {
entries.unshift({
name, sfn: name,
stat: { isDirectory: () => false, size: bytes.length, mtime: now },
});
}
}
for (const [k, v] of aliases) sfnMap.set(k, v);
return entries;
}
@@ -148,7 +240,13 @@ export class SmbSession {
case CMD_LOGOFF_ANDX: return this.logoff(req);
case CMD_NT_CREATE_ANDX: return this.ntCreate(req);
case CMD_OPEN_ANDX: return this.openAndx(req);
case CMD_READ: return this.coreRead(req);
// READ_RAW reply has no SMB header — handle outside the generic
// catch so an fs error becomes a 0-byte frame, not garbage data.
case CMD_READ_RAW:
try { return this.readRaw(req); } catch { return new Uint8Array(0); }
case CMD_READ_ANDX: return this.read(req);
case CMD_SEEK: return this.seek(req);
case CMD_CLOSE: return this.close(req);
case CMD_TRANSACTION: return this.transRap(req);
case CMD_TRANSACTION2: return this.trans2(req);
@@ -186,26 +284,51 @@ export class SmbSession {
}
log("dialects offered:", offered);
let pick = -1;
let pick = -1, picked = "";
for (const d of DIALECTS) {
const idx = offered.indexOf(d);
if (idx >= 0) { pick = idx; break; }
if (idx >= 0) { pick = idx; picked = d; break; }
}
if (pick < 0) {
// refuse — but Win95 always offers at least LANMAN
const w = new Writer().u16(0xffff).build();
return buildSmb(req, CMD_NEGOTIATE, 0, w, new Uint8Array(0));
}
log(`negotiate → "${picked}" (idx ${pick})`);
// LM 2.1 / NT-compatible response (13 words). We claim share-level
// security (no challenge), no encryption, modest buffer.
if (picked === NT_DIALECT) {
// NT LM 0.12 response: 17 words. Capabilities = 0 keeps Win95 on the
// codepaths we already implement: OEM strings (no CAP_UNICODE), DOS
// errors (no CAP_NT_STATUS), SMB_INFO_STANDARD find (no CAP_NT_FIND),
// OPEN_ANDX over NT_CREATE (no CAP_NT_SMBS). The dialect alone is what
// flips the redirector from CMD_SEARCH to TRANS2/FIND_FIRST2.
const words = new Writer()
.u16(pick) // DialectIndex
.u8(0x00) // SecurityMode: share-level, no challenge
.u16(1) // MaxMpxCount
.u16(1) // MaxNumberVcs
.u32(16384) // MaxBufferSize
.u32(65535) // MaxRawSize
.u32(0) // SessionKey
.u32(0x00000001) // Capabilities: CAP_RAW_MODE only
.u64(0) // SystemTime (FILETIME — Win95 ignores 0)
.u16(0) // ServerTimeZone
.u8(0) // ChallengeLength = 0
.build();
const bytes = new Writer().cstr("WORKGROUP").build();
// FLAGS2_LONG_NAMES on the negotiate reply itself signals "I can return
// long names" — Win95 keys its FIND_FIRST2 info-level on this bit.
return buildSmb(req, CMD_NEGOTIATE, 0, words, bytes, { flags2: 0x0001 });
}
// LM 2.1 response (13 words). Share-level security, no challenge.
const words = new Writer()
.u16(pick) // DialectIndex
.u16(0x0000) // SecurityMode: share-level, no challenge
.u16(16384) // MaxBufferSize
.u16(1) // MaxMpxCount
.u16(1) // MaxNumberVcs
.u16(0) // RawMode (none)
.u16(0x0001) // RawMode: read-raw supported
.u32(0) // SessionKey
.u16(0) // ServerTime (we cheat — Win95 doesn't care)
.u16(0) // ServerDate
@@ -252,11 +375,13 @@ export class SmbSession {
const service = br.cstr();
log(`tree connect: path="${reqPath}" service="${service}"`);
// Accept anything for now — share name extraction is a refinement.
// IPC$ is special (named pipes); we pretend to support it so the
// redirector doesn't bail, but file ops on tid 0xfffe will error out.
const isIpc = /\\IPC\$$/i.test(reqPath);
this.tid = isIpc ? 0xfffe : 1;
// Path is \\SERVER\SHARE — route by the share segment. Unknown names fall
// through to the user share so a stale `net use` (e.g. from before the
// user re-pointed the mounted folder) still connects to *something*, and
// so W95TOOLS.EXE can hard-code \\HOST\HOST when it auto-maps Z:.
const share = reqPath.split(/[\\\/]/).pop()?.toUpperCase() ?? "";
const isIpc = share === "IPC$";
this.tid = isIpc ? TID_IPC : share === TOOLS_SHARE ? TID_TOOLS : TID_SHARE;
const words = new Writer()
.bytes(andxNone())
@@ -290,7 +415,11 @@ export class SmbSession {
// each component is mapped through the SFN table. Refuses traversal,
// including via symlinks.
// ───────────────────────────────────────────────────────────────────────────
private resolve(smbPath: string): string | null {
private resolve(tid: number, smbPath: string): string | null {
const root = tid === TID_SHARE ? this.realRoot
: tid === TID_TOOLS ? this.toolsRoot
: undefined;
if (!root) return null;
let p = smbPath.replace(/\\/g, "/");
if (p.startsWith("/")) p = p.slice(1);
@@ -299,7 +428,7 @@ export class SmbSession {
// names (if the client somehow learned them) or they'll fail existence
// checks below.
const parts = p ? p.split("/") : [];
let cur = this.realRoot;
let cur = root;
for (const part of parts) {
if (!part || part === ".") continue;
const map = this.sfnMaps.get(cur);
@@ -309,7 +438,7 @@ export class SmbSession {
const candidate = cur;
// Lexical check first — fast reject for ../../ without touching disk
const lex = path.relative(this.realRoot, candidate);
const lex = path.relative(root, candidate);
if (lex.startsWith("..") || path.isAbsolute(lex)) return null;
// Symlink check: realpath the deepest existing ancestor, then re-append
@@ -330,7 +459,7 @@ export class SmbSession {
}
const real = tail ? path.join(probe, tail) : probe;
if (real !== this.realRoot && !real.startsWith(this.realRoot + path.sep)) {
if (real !== root && !real.startsWith(root + path.sep)) {
return null;
}
return real;
@@ -349,7 +478,7 @@ export class SmbSession {
private queryInfo(req: SmbHeader): Uint8Array {
const smbPath = this.smbPathFromBytes(req);
const v = this.getVirtual(smbPath);
const v = this.getVirtual(req.tid, smbPath);
if (v) {
const words = new Writer()
.u16(ATTR_ARCHIVE)
@@ -360,15 +489,14 @@ export class SmbSession {
return buildSmb(req, CMD_QUERY_INFORMATION, 0, words, new Uint8Array(0));
}
const hostPath = this.resolve(smbPath);
const hostPath = this.resolve(req.tid, smbPath);
if (!hostPath || !fs.existsSync(hostPath)) {
return buildSmb(req, CMD_QUERY_INFORMATION, dosError(ERRDOS, ERR_BADFILE),
new Uint8Array(0), new Uint8Array(0));
}
const st = fs.statSync(hostPath);
const attrs = st.isDirectory() ? ATTR_DIRECTORY : ATTR_ARCHIVE;
const words = new Writer()
.u16(attrs)
.u16(hostAttrs(path.basename(hostPath), st.isDirectory()))
.u32(unixToSmbTime(st.mtime))
.u32(Math.min(st.size, 0xffffffff))
.zero(10) // reserved
@@ -378,12 +506,7 @@ export class SmbSession {
private checkDirectory(req: SmbHeader): Uint8Array {
const smbPath = this.smbPathFromBytes(req);
// Virtuals are files — explicitly NOT directories
if (this.getVirtual(smbPath)) {
return buildSmb(req, CMD_CHECK_DIRECTORY, dosError(ERRDOS, ERR_BADPATH),
new Uint8Array(0), new Uint8Array(0));
}
const hostPath = this.resolve(smbPath);
const hostPath = this.resolve(req.tid, smbPath);
if (!hostPath || !fs.existsSync(hostPath) || !fs.statSync(hostPath).isDirectory()) {
return buildSmb(req, CMD_CHECK_DIRECTORY, dosError(ERRDOS, ERR_BADPATH),
new Uint8Array(0), new Uint8Array(0));
@@ -423,16 +546,15 @@ export class SmbSession {
}
private doOpen(req: SmbHeader, cmd: number, smbPath: string): Uint8Array {
// Virtual root files first — they shadow anything on disk with the same name
const vbytes = this.getVirtual(smbPath);
const vbytes = this.getVirtual(req.tid, smbPath);
if (vbytes) {
const fid = this.nextFid++;
this.fids.set(fid, { hostPath: `<virtual>${smbPath}`, fd: -1, size: vbytes.length, isDir: false, virtual: vbytes });
log(`open "${smbPath}" → virtual (${vbytes.length} bytes)`);
return this.buildOpenReply(req, cmd, fid, false, vbytes.length, new Date());
return this.buildOpenReply(req, cmd, fid, ATTR_ARCHIVE, vbytes.length, new Date());
}
const hostPath = this.resolve(smbPath);
const hostPath = this.resolve(req.tid, smbPath);
log(`open "${smbPath}" → ${hostPath}`);
if (!hostPath || !fs.existsSync(hostPath)) {
return buildSmb(req, cmd, dosError(ERRDOS, ERR_BADFILE),
@@ -443,17 +565,18 @@ export class SmbSession {
const isDir = st.isDirectory();
const fd = isDir ? -1 : fs.openSync(hostPath, "r");
this.fids.set(fid, { hostPath, fd, size: st.size, isDir });
return this.buildOpenReply(req, cmd, fid, isDir, st.size, st.mtime);
return this.buildOpenReply(req, cmd, fid,
hostAttrs(path.basename(hostPath), isDir), st.size, st.mtime);
}
private buildOpenReply(req: SmbHeader, cmd: number, fid: number, isDir: boolean, size: number, mtime: Date): Uint8Array {
private buildOpenReply(req: SmbHeader, cmd: number, fid: number, attrs: number, size: number, mtime: Date): Uint8Array {
const isDir = (attrs & ATTR_DIRECTORY) !== 0;
const sz = Math.min(size, 0xffffffff);
if (cmd === CMD_OPEN_ANDX) {
const words = new Writer()
.bytes(andxNone())
.u16(fid)
.u16(isDir ? ATTR_DIRECTORY : ATTR_ARCHIVE)
.u16(attrs)
.u32(unixToSmbTime(mtime))
.u32(sz)
.u16(0) // GrantedAccess: read
@@ -476,7 +599,7 @@ export class SmbSession {
.u64(0) // LastAccessTime
.u64(0) // LastWriteTime
.u64(0) // ChangeTime
.u32(isDir ? ATTR_DIRECTORY : ATTR_ARCHIVE) // ExtFileAttributes
.u32(attrs) // ExtFileAttributes
.u64(sz) // AllocationSize
.u64(sz) // EndOfFile
.u16(0) // FileType: disk
@@ -532,6 +655,67 @@ export class SmbSession {
return buildSmb(req, CMD_READ_ANDX, 0, words, bytes);
}
// READ (0x0a): the original core-protocol read. With Capabilities=0 in our
// NT negotiate, Win95's redirector uses this instead of READ_ANDX.
// Request words: FID(2) Count(2) Offset(4) Remaining(2).
// Response words: Count(2) Reserved(8); bytes: 0x01 DataLen(2) Data.
private coreRead(req: SmbHeader): Uint8Array {
const wr = new Reader(req.words);
const fid = wr.u16();
const count = wr.u16();
const offset = wr.u32();
const data = this.readBytes(fid, offset, count);
if (!data) {
return buildSmb(req, CMD_READ, dosError(ERRDOS, ERR_BADFID),
new Uint8Array(0), new Uint8Array(0));
}
const words = new Writer().u16(data.length).zero(8).build();
const bytes = new Writer().u8(0x01).u16(data.length).bytes(data).build();
return buildSmb(req, CMD_READ, 0, words, bytes);
}
private readBytes(fid: number, offset: number, count: number, cap = 16384): Uint8Array | null {
const file = this.fids.get(fid);
if (!file || file.isDir) return null;
const want = Math.min(count, cap, Math.max(0, file.size - offset));
if (file.virtual) return file.virtual.slice(offset, offset + want);
const buf = Buffer.alloc(want);
const n = want > 0 ? fs.readSync(file.fd, buf, 0, want, offset) : 0;
return buf.subarray(0, n);
}
// READ_RAW (0x1a): Win95's bulk-transfer path. The response is *not* an SMB
// message — just the raw file bytes inside a NetBIOS frame, length implied
// by the NB header. On error/EOF we send zero bytes and the client falls
// back to a normal READ to get the actual error code.
private readRaw(req: SmbHeader): Uint8Array {
const wr = new Reader(req.words);
const fid = wr.u16();
const offset = wr.u32();
const maxCount = wr.u16();
return this.readBytes(fid, offset, maxCount, 65535) ?? new Uint8Array(0);
}
// SEEK (0x12): legacy lseek. READ_ANDX carries an explicit offset so we
// don't need a real cursor — but Win95 (Notepad in particular) opens,
// SEEKs to end-of-file with mode=2 offset=0 to learn the size, then
// re-opens for the actual read. ERR_BADFUNC here makes Explorer wedge.
private seek(req: SmbHeader): Uint8Array {
const wr = new Reader(req.words);
const fid = wr.u16();
const mode = wr.u16();
const off = wr.u32() | 0; // signed
const file = this.fids.get(fid);
if (!file) {
return buildSmb(req, CMD_SEEK, dosError(ERRDOS, ERR_BADFID),
new Uint8Array(0), new Uint8Array(0));
}
const base = mode === 2 ? file.size : mode === 1 ? (file.pos ?? 0) : 0;
file.pos = Math.max(0, base + off);
const words = new Writer().u32(Math.min(file.pos, 0xffffffff)).build();
return buildSmb(req, CMD_SEEK, 0, words, new Uint8Array(0));
}
private close(req: SmbHeader): Uint8Array {
const wr = new Reader(req.words);
const fid = wr.u16();
@@ -577,23 +761,22 @@ export class SmbSession {
const lastSep = Math.max(pattern.lastIndexOf("\\"), pattern.lastIndexOf("/"));
const dirPart = lastSep >= 0 ? pattern.slice(0, lastSep) : "";
const namePart = lastSep >= 0 ? pattern.slice(lastSep + 1) : pattern;
const hostDir = this.resolve(dirPart || "\\");
log(`SEARCH "${pattern}" → ${hostDir}`);
if (!hostDir || !fs.existsSync(hostDir)) {
log(`SEARCH "${pattern}" tid=${req.tid}`);
const listed = this.listForSearch(req.tid, dirPart);
if (!listed) {
return buildSmb(req, CMD_SEARCH, dosError(ERRDOS, ERR_BADPATH),
new Uint8Array(0), new Uint8Array(0));
}
const { all, dotStat } = listed;
const matcher = wildcardMatcher(namePart);
const all = this.listDir(hostDir);
// Match against the SFN — that's what the client sees and asks for
const entries = all.filter(e => matcher(e.sfn));
// . and .. only for wildcard listings — a single-name SEARCH is a stat
// probe and must return exactly the matching file or nothing.
if (/[*?]/.test(namePart)) {
const dotStat = fs.statSync(hostDir);
entries.unshift(
{ name: "..", sfn: "..", stat: dotStat },
{ name: ".", sfn: ".", stat: dotStat },
{ name: "..", sfn: "..", attr: ATTR_DIRECTORY, stat: dotStat },
{ name: ".", sfn: ".", attr: ATTR_DIRECTORY, stat: dotStat },
);
}
sid = this.nextSid++;
@@ -625,7 +808,7 @@ export class SmbSession {
out.u8(sid & 0xff).u8(sid >> 8).u8(nextIdx & 0xff).u8(nextIdx >> 8);
out.zero(21 - 4);
// Attrs
out.u8(e.stat.isDirectory() ? ATTR_DIRECTORY : ATTR_ARCHIVE);
out.u8(e.attr);
// Time/Date
const dt = unixToDosDateTime(e.stat.mtime);
out.u16(dt.time).u16(dt.date);
@@ -685,6 +868,7 @@ export class SmbSession {
switch (subCmd) {
case TRANS2_FIND_FIRST2: return this.findFirst(req, params);
case TRANS2_FIND_NEXT2: return this.findNext(req, params);
case TRANS2_QUERY_FS_INFO: return this.queryFsInfo(req, params);
case TRANS2_QUERY_PATH_INFO: return this.queryPathInfo(req, params);
default:
return buildSmb(req, CMD_TRANSACTION2, dosError(ERRSRV, ERR_BADFUNC),
@@ -697,49 +881,50 @@ export class SmbSession {
// SearchStorageType(4) FileName(string)
const pr = new Reader(params);
pr.u16(); // searchAttrs
pr.u16(); // searchCount
pr.u16(); // flags
const searchCount = pr.u16();
const findFlags = pr.u16();
const infoLevel = pr.u16();
pr.u32(); // storageType
const pattern = (req.flags2 & 0x8000) ? pr.ucs2() : pr.cstr();
log(`FIND_FIRST2 level=0x${infoLevel.toString(16)} pattern="${pattern}"`);
log(`FIND_FIRST2 level=0x${infoLevel.toString(16)} flags=0x${findFlags.toString(16)} pattern="${pattern}"`);
// pattern is like "\dir\*" or "\*" or "\file.txt"
const lastSep = Math.max(pattern.lastIndexOf("\\"), pattern.lastIndexOf("/"));
const dirPart = lastSep >= 0 ? pattern.slice(0, lastSep) : "";
const namePart = lastSep >= 0 ? pattern.slice(lastSep + 1) : pattern;
const hostDir = this.resolve(dirPart || "\\");
if (!hostDir || !fs.existsSync(hostDir)) {
const listed = this.listForSearch(req.tid, dirPart);
if (!listed) {
return buildSmb(req, CMD_TRANSACTION2, dosError(ERRDOS, ERR_BADPATH),
new Uint8Array(0), new Uint8Array(0));
}
const { all, dotStat } = listed;
const matcher = wildcardMatcher(namePart);
const all = this.listDir(hostDir);
const entries = all.filter(e => matcher(e.sfn) || matcher(e.name));
if (/[*?]/.test(namePart)) {
const dotStat = fs.statSync(hostDir);
entries.unshift(
{ name: "..", sfn: "..", stat: dotStat },
{ name: ".", sfn: ".", stat: dotStat },
{ name: "..", sfn: "..", attr: ATTR_DIRECTORY, stat: dotStat },
{ name: ".", sfn: ".", attr: ATTR_DIRECTORY, stat: dotStat },
);
}
const sid = this.nextSid++;
this.sids.set(sid, { entries, idx: 0 });
return this.findReply(req, sid, infoLevel, true);
return this.findReply(req, sid, infoLevel, findFlags, searchCount, true);
}
private findNext(req: SmbHeader, params: Uint8Array): Uint8Array {
const pr = new Reader(params);
const sid = pr.u16();
pr.u16(); // searchCount
const searchCount = pr.u16();
const infoLevel = pr.u16();
// ResumeKey(4) Flags(2) FileName — we just continue from where we left off
return this.findReply(req, sid, infoLevel, false);
pr.u32(); // resumeKey
const findFlags = pr.u16();
return this.findReply(req, sid, infoLevel, findFlags, searchCount, false);
}
private findReply(req: SmbHeader, sid: number, _infoLevel: number, isFirst: boolean): Uint8Array {
private findReply(req: SmbHeader, sid: number, infoLevel: number,
findFlags: number, maxCount: number, isFirst: boolean): Uint8Array {
const search = this.sids.get(sid);
if (!search || search.idx >= search.entries.length) {
this.sids.delete(sid);
@@ -747,33 +932,89 @@ export class SmbSession {
new Uint8Array(0), new Uint8Array(0));
}
// We return SMB_INFO_STANDARD (level 1) regardless of what was asked —
// Win95 accepts this. Each entry: ResumeKey(4) CreationDate(2) CreationTime(2)
// LastAccessDate(2) LastAccessTime(2) LastWriteDate(2) LastWriteTime(2)
// DataSize(4) AllocationSize(4) Attributes(2) FileNameLength(1) FileName
// Max ~500 bytes per entry batch to keep under our buffer cap.
const data = new Writer();
let count = 0;
let lastNameOffset = 0;
while (search.idx < search.entries.length && data.length < 8000) {
const e = search.entries[search.idx++];
const dosDate = unixToDosDateTime(e.stat.mtime);
const sz = Math.min(e.stat.size, 0xffffffff);
const entryStart = data.length;
data.u32(search.idx); // ResumeKey
data.u16(dosDate.date).u16(dosDate.time); // create
data.u16(dosDate.date).u16(dosDate.time); // access
data.u16(dosDate.date).u16(dosDate.time); // write
data.u32(sz);
data.u32(sz);
data.u16(e.stat.isDirectory() ? ATTR_DIRECTORY : ATTR_ARCHIVE);
data.u8(e.name.length);
lastNameOffset = data.length - entryStart;
data.cstr(e.name);
count++;
// Win95 sends SearchCount=6 and MaxDataCount≈2.4KB; blow past either and
// VREDIR drops the whole TCP session ("network resource no longer
// available"). Cap on count, then a byte ceiling well under MaxDataCount
// as a backstop for pathological filenames.
const fits = () =>
search.idx < search.entries.length && count < maxCount && data.length < 2000;
if (infoLevel === 0x104) {
// SMB_FIND_FILE_BOTH_DIRECTORY_INFO — what Win95 asks for under
// NT LM 0.12 regardless of CAP_NT_FIND. 94 bytes fixed + long name,
// 4-byte aligned. NextEntryOffset chains entries; 0 terminates.
// ShortName is *always* UTF-16LE per spec even though FileName stays
// OEM (we never set CAP_UNICODE). 0x10x levels never take a resume-key
// prefix.
let prevStart = -1;
while (fits()) {
const e = search.entries[search.idx++];
const ft = unixToFiletime(e.stat.mtime);
const sz = e.stat.isDirectory() ? 0 : Math.min(e.stat.size, 0xffffffff);
const entryStart = data.length;
if (prevStart >= 0) data.patch32(prevStart, entryStart - prevStart);
prevStart = entryStart;
data.u32(0); // NextEntryOffset — patched on next iter
data.u32(search.idx); // FileIndex
data.u64(ft.lo, ft.hi); // CreationTime
data.u64(ft.lo, ft.hi); // LastAccessTime
data.u64(ft.lo, ft.hi); // LastWriteTime
data.u64(ft.lo, ft.hi); // ChangeTime
data.u64(sz); // EndOfFile
data.u64(sz); // AllocationSize
data.u32(e.attr);
// Samba (win9x-tested) writes FileName null-terminated AND counts the
// null in FileNameLength; vredir copies the resume name as a C string
// from LastNameOffset, so an unterminated name reads past the buffer.
data.u32(e.name.length + 1);
data.u32(0); // EaSize
// ShortNameLength=0 always. MS-CIFS says ShortName is UCS-2 even in
// an OEM session, but Win95's redirector reads it as OEM when
// FLAGS2_UNICODE is clear — a non-empty UCS-2 name here makes
// shell32 GPF on the single-directory probe it does when navigating
// into a subfolder (root listing survives because Explorer never
// looks at this field there). Explorer doesn't need the short name
// anyway; it has the long one.
data.u8(0);
data.u8(0); // Reserved
data.zero(24); // ShortName WCHAR[12]
lastNameOffset = data.length;
for (let k = 0; k < e.name.length; k++) data.u8(e.name.charCodeAt(k));
data.u8(0);
data.zero((4 - (data.length & 3)) & 3);
count++;
}
} else {
// SMB_INFO_STANDARD (level 1). Each entry: [ResumeKey(4)] CDate(2)
// CTime(2) ADate(2) ATime(2) WDate(2) WTime(2) Size(4) Alloc(4)
// Attrs(2) NameLen(1) Name\0. ResumeKey prefix is ONLY present if the
// client set SMB_FIND_RETURN_RESUME_KEYS — emit it unconditionally
// and the client misparses every entry by 4 bytes.
const wantResumeKey = (findFlags & 0x0004) !== 0;
while (fits()) {
const e = search.entries[search.idx++];
const dosDate = unixToDosDateTime(e.stat.mtime);
const sz = Math.min(e.stat.size, 0xffffffff);
const entryStart = data.length;
if (wantResumeKey) data.u32(search.idx);
data.u16(dosDate.date).u16(dosDate.time);
data.u16(dosDate.date).u16(dosDate.time);
data.u16(dosDate.date).u16(dosDate.time);
data.u32(sz);
data.u32(sz);
data.u16(e.attr);
data.u8(e.name.length);
lastNameOffset = data.length - entryStart;
data.cstr(e.name);
count++;
}
}
const eos = search.idx >= search.entries.length;
if (eos) this.sids.delete(sid);
log(`${count} entries, ${data.length} bytes, eos=${eos}`);
// params reply differs: FIND_FIRST has SID(2), FIND_NEXT doesn't
const pw = new Writer();
@@ -785,14 +1026,68 @@ export class SmbSession {
return this.trans2Reply(req, pw.build(), data.build());
}
private queryFsInfo(req: SmbHeader, params: Uint8Array): Uint8Array {
const level = params[0] | (params[1] << 8);
log(`QUERY_FS_INFO level=0x${level.toString(16)}`);
let data: Uint8Array;
switch (level) {
case 0x0105: { // SMB_QUERY_FS_ATTRIBUTE_INFO
// "FAT", not "NTFS" — shell32 keys NTFS-specific property/security
// handlers on this string, and they fault on subfolder entry when
// the backing protocol can't answer their follow-ups.
const fsName = "FAT";
data = new Writer()
.u32(0x00000002) // FILE_CASE_PRESERVED_NAMES — the bit Win95 reads to decide LFN-capable
.u32(255) // MaxFileNameLengthInBytes
.u32(fsName.length)
.bytes([...fsName].map(c => c.charCodeAt(0)))
.build();
break;
}
case 0x0001: { // SMB_INFO_ALLOCATION
data = new Writer()
.u32(0) // idFileSystem
.u32(8) // SectorsPerAllocationUnit
.u32(0x10000) // TotalAllocationUnits — fake but plausible
.u32(0x08000) // TotalFreeAllocationUnits
.u16(512) // BytesPerSector
.build();
break;
}
case 0x0002: { // SMB_INFO_VOLUME
const label = req.tid === TID_TOOLS ? TOOLS_SHARE : this.shareName;
data = new Writer()
.u32(0) // VolumeSerialNumber
.u8(label.length)
.bytes([...label].map(c => c.charCodeAt(0)))
.build();
break;
}
default:
return buildSmb(req, CMD_TRANSACTION2, dosError(ERRDOS, 124 /* ERROR_INVALID_LEVEL */),
new Uint8Array(0), new Uint8Array(0));
}
return this.trans2Reply(req, new Uint8Array(0), data);
}
private queryPathInfo(req: SmbHeader, params: Uint8Array): Uint8Array {
// params: InfoLevel(2) Reserved(4) FileName
const pr = new Reader(params);
const level = pr.u16();
pr.u32();
const smbPath = (req.flags2 & 0x8000) ? pr.ucs2() : pr.cstr();
const hostPath = this.resolve(smbPath);
log(`QUERY_PATH_INFO level=0x${level.toString(16)} "${smbPath}"`);
const v = this.getVirtual(req.tid, smbPath);
if (v) {
const dd = unixToDosDateTime(new Date());
const data = new Writer()
.u16(dd.date).u16(dd.time).u16(dd.date).u16(dd.time).u16(dd.date).u16(dd.time)
.u32(v.length).u32(v.length)
.u16(ATTR_ARCHIVE)
.build();
return this.trans2Reply(req, new Writer().u16(0).build(), data);
}
const hostPath = this.resolve(req.tid, smbPath);
if (!hostPath || !fs.existsSync(hostPath)) {
return buildSmb(req, CMD_TRANSACTION2, dosError(ERRDOS, ERR_BADFILE),
new Uint8Array(0), new Uint8Array(0));
@@ -806,7 +1101,7 @@ export class SmbSession {
.u16(dosDate.date).u16(dosDate.time)
.u16(dosDate.date).u16(dosDate.time)
.u32(sz).u32(sz)
.u16(st.isDirectory() ? ATTR_DIRECTORY : ATTR_ARCHIVE)
.u16(hostAttrs(path.basename(hostPath), st.isDirectory()))
.build();
const replyParams = new Writer().u16(0).build(); // EaErrorOffset
return this.trans2Reply(req, replyParams, data);
@@ -868,7 +1163,8 @@ export class SmbSession {
// W = 2-byte type (0=disk, 3=IPC)
// z = 4-byte string pointer (we send 0 = no remark)
const shares = [
{ name: "HOST", type: 0 },
{ name: this.shareName, type: 0 },
{ name: TOOLS_SHARE, type: 0 },
{ name: "IPC$", type: 3 },
];
const data = new Writer();
@@ -924,14 +1220,19 @@ export class SmbSession {
/** Build the TRANS2 response envelope. Tedious but mechanical. */
private trans2Reply(req: SmbHeader, params: Uint8Array, data: Uint8Array): Uint8Array {
// 10 words + 1 setup word, then bytes = pad + params + pad + data
// Offsets are from SMB header start (32 bytes before word_count byte).
const wc = 10 + 1; // SetupCount=1 → 1 setup word
const wordBlockSize = 1 + wc * 2 + 2; // wc byte + words + bcc
// bytes block: pad to align params (we don't bother), params, pad, data
const paramOffset = 32 + wordBlockSize;
const dataOffset = paramOffset + params.length;
// 10 words + 0 setup, then bytes = pad + params + pad + data. Both pads
// bring the following block to a 4-byte boundary from the SMB header
// start — Win95's redirector copies params/data via REP MOVSD and an
// odd offset shifts the entire 0x104 record by up to 3 bytes inside its
// buffer, which explorer survives at the root but GPFs on the second
// single-entry probe when navigating into a subfolder.
const wc = 10;
const bytesStart = 32 + 1 + wc * 2 + 2; // header + wc + words + bcc
const align4 = (n: number) => (4 - (n & 3)) & 3;
const pad1 = align4(bytesStart);
const paramOffset = bytesStart + pad1;
const pad2 = align4(paramOffset + params.length);
const dataOffset = paramOffset + params.length + pad2;
const words = new Writer()
.u16(params.length) // TotalParamCount
@@ -943,14 +1244,13 @@ export class SmbSession {
.u16(data.length) // DataCount
.u16(dataOffset) // DataOffset
.u16(0) // DataDisplacement
.u8(1) // SetupCount
.u8(0) // SetupCount
.u8(0) // Reserved
.u16(0) // Setup[0]
.build();
const bytes = new Uint8Array(params.length + data.length);
bytes.set(params, 0);
bytes.set(data, params.length);
const bytes = new Uint8Array(pad1 + params.length + pad2 + data.length);
bytes.set(params, pad1);
bytes.set(data, pad1 + params.length + pad2);
return buildSmb(req, CMD_TRANSACTION2, 0, words, bytes);
}
@@ -966,6 +1266,39 @@ export class SmbSession {
// ─── helpers ─────────────────────────────────────────────────────────────────
// DOS device names. Win95 maps these regardless of extension or directory,
// so a host file called "con.txt" or "AUX" opens the device and hangs the
// redirector. Guarded in both the long name and the generated 8.3 name.
const DOS_RESERVED = /^(CON|PRN|AUX|NUL|CLOCK\$|COM[1-9]|LPT[1-9])$/i;
/** Map a host filename to something Win95 can safely display and round-trip. */
function displayName(real: string): string {
// Per-char: single-byte only, no Windows-reserved chars, no controls
// (including DEL/C1). Multi-code-unit chars collapse to one '_'.
const bad = /[<>:"/\\|?*\x00-\x1f\x7f-\x9f]/;
let out = "";
for (const ch of real) { // iterates code points, so 🎨 is one step
const c = ch.codePointAt(0)!;
out += c > 0xff || bad.test(ch) ? "_" : ch;
}
// Win95 silently strips trailing dots/spaces, so "foo." would alias "foo".
// Replace the trailing run so the name stays distinct and length-stable.
out = out.replace(/[. ]+$/, m => "_".repeat(m.length));
// Reserved device basenames get a suffix so the guest never sees a bare CON.
// Win95 tests the component before the *first* dot and ignores trailing
// spaces there, so "nul.tar.gz" and "con .txt" both need guarding.
const dot = out.indexOf(".");
const base = dot < 0 ? out : out.slice(0, dot);
if (DOS_RESERVED.test(base.replace(/ +$/, ""))) {
out = base + "_" + (dot < 0 ? "" : out.slice(dot));
}
return out || "_";
}
function isRootPath(smbPath: string): boolean {
return smbPath.replace(/[\\\/]/g, "") === "";
}
function wildcardMatcher(pattern: string): (name: string) => boolean {
// SMB wildcards: * = any, ? = one char, also ">"/"<"/"\"" exist but
// Win95 mostly sends *.* or * — collapse *.* → *
@@ -1052,6 +1385,12 @@ function unixToSmbTime(d: Date): number {
return Math.floor(d.getTime() / 1000);
}
/** NT FILETIME: 100ns ticks since 1601-01-01, split into two u32s. */
function unixToFiletime(d: Date): { lo: number; hi: number } {
const t = BigInt(d.getTime()) * 10000n + 116444736000000000n;
return { lo: Number(t & 0xffffffffn), hi: Number(t >> 32n) };
}
function clean83(s: string): string {
return s.replace(/[^A-Za-z0-9_$~!#%&'()@^`{}-]/g, "").toUpperCase();
}
@@ -1059,10 +1398,12 @@ function clean83(s: string): string {
/** True if the name already fits 8.3 with no lossy transformation. */
function fits83(name: string): boolean {
if (name === "." || name === "..") return true;
if (/[. ]$/.test(name)) return false;
const dot = name.lastIndexOf(".");
const base = dot > 0 ? name.slice(0, dot) : name;
const ext = dot > 0 ? name.slice(dot + 1) : "";
return base.length > 0 && base.length <= 8 && ext.length <= 3 &&
!DOS_RESERVED.test(base) &&
clean83(base).length === base.length &&
clean83(ext).length === ext.length;
}
@@ -1093,7 +1434,7 @@ function buildSfnMap(names: string[]): Map<string, string> {
const extRaw = dot > 0 ? real.slice(dot + 1) : "";
const ext = clean83(extRaw).slice(0, 3);
let base = clean83(baseRaw);
if (base.length === 0) base = "_";
if (base.length === 0 || DOS_RESERVED.test(base)) base += "_";
// Windows uses 6 chars + ~N for N<10, then 5+~NN, etc. Good enough.
for (let n = 1; ; n++) {

View File

@@ -16,7 +16,10 @@ export const CMD_TREE_DISCONNECT = 0x71;
export const CMD_LOGOFF_ANDX = 0x74;
export const CMD_NT_CREATE_ANDX = 0xa2;
export const CMD_OPEN_ANDX = 0x2d;
export const CMD_READ = 0x0a;
export const CMD_READ_RAW = 0x1a;
export const CMD_READ_ANDX = 0x2e;
export const CMD_SEEK = 0x12;
export const CMD_CLOSE = 0x04;
export const CMD_TRANSACTION = 0x25;
export const CMD_TRANSACTION2 = 0x32;
@@ -30,6 +33,7 @@ export const CMD_SEARCH = 0x81;
// TRANS2 subcommands
export const TRANS2_FIND_FIRST2 = 0x01;
export const TRANS2_FIND_NEXT2 = 0x02;
export const TRANS2_QUERY_FS_INFO = 0x03;
export const TRANS2_QUERY_PATH_INFO = 0x05;
export const TRANS2_QUERY_FILE_INFO = 0x07;
@@ -142,6 +146,9 @@ export const cmdName: Record<number, string> = {
[CMD_NT_CREATE_ANDX]: "NT_CREATE",
[CMD_OPEN_ANDX]: "OPEN",
[CMD_READ_ANDX]: "READ",
[CMD_READ_RAW]: "READ_RAW",
[CMD_READ]: "READ",
[CMD_SEEK]: "SEEK",
[CMD_CLOSE]: "CLOSE",
[CMD_TRANSACTION]: "TRANS(RAP)",
[CMD_TRANSACTION2]: "TRANS2",

View File

@@ -1,6 +1,6 @@
// Standalone test of the SMB stack — no v86, no Electron. Feeds canned
// requests through NetBIOSFramer + SmbSession and inspects responses.
// Run: npx ts-node src/renderer/smb/test-standalone.ts
// Run: see src/renderer/smb/README.md for the ts-node invocation.
import * as fs from "fs";
import * as path from "path";
@@ -93,12 +93,18 @@ console.log("\n[2] NEGOTIATE");
ok(parsed.cmd === CMD_NEGOTIATE, "cmd echoed");
ok((parsed.flags & 0x80) !== 0, "reply flag set");
ok(parsed.status === 0, "status OK");
ok(parsed.wordCount === 13, "13-word LM response");
// word[0] = dialect index — we pick LANMAN2.1 (idx 3) since our 13-word
// response is the LANMAN format; picking NT LM 0.12 would require the
// 17-word NT response which we don't implement
ok(parsed.wordCount === 17, "17-word NT response");
// word[0] = dialect index — NT LM 0.12 is idx 4 and gets the 17-word
// response; the 13-word LM shape is now only emitted as a fallback.
const pickedIdx = parsed.words[0] | (parsed.words[1] << 8);
ok(pickedIdx === 3, `picked LANMAN2.1 (idx ${pickedIdx})`);
ok(pickedIdx === 4, `picked NT LM 0.12 (idx ${pickedIdx})`);
// Fallback: a client that doesn't offer NT LM 0.12 still gets the 13-word
// LANMAN response.
const lmBytes: number[] = [];
for (const d of dialects.slice(0, 4)) { lmBytes.push(0x02); lmBytes.push(...cstr(d)); }
const lmParsed = parseSmb(session.handle(smbReq(CMD_NEGOTIATE, [], lmBytes))!)!;
ok(lmParsed.wordCount === 13, "13-word LM fallback");
}
// ─── Test 3: SESSION_SETUP ───────────────────────────────────────────────────
@@ -170,15 +176,133 @@ console.log("\n[5] TRANS2 FIND_FIRST2");
const pStart = replyParamOffset - replyBytesStart;
const replyParams = parsed.bytes.slice(pStart, pStart + replyParamCount);
const searchCount = replyParams[2] | (replyParams[3] << 8);
// Should find: . .. _MAPZ.BAT(virtual) hello.txt subdir = 5
ok(searchCount === 5, `found ${searchCount} entries (expect 5)`);
// Should find: . .. hello.txt subdir = 4 (virtuals moved to TOOLS share)
ok(searchCount === 4, `found ${searchCount} entries (expect 4)`);
// Data block has the entries — just verify they're in there somewhere
const dataStr = String.fromCharCode(...parsed.bytes);
ok(dataStr.includes("_MAPZ.BAT"), "virtual _MAPZ.BAT in listing");
ok(!dataStr.includes("_MAPZ.BAT"), "no virtual leak in user share");
ok(dataStr.includes("hello.txt"), "hello.txt in listing");
ok(dataStr.includes("subdir"), "subdir in listing");
}
// ─── Test 5b: FIND_FIRST2 level 0x104 (LFN) ──────────────────────────────────
console.log("\n[5b] TRANS2 FIND_FIRST2 level=0x104");
{
fs.writeFileSync(path.join(tmpRoot, "A Long Filename Here.txt"), "lfn");
// Same envelope as [5] but InfoLevel=0x104
const t2params = [...u16(0x16), ...u16(100), ...u16(0), ...u16(0x104),
...u32(0), ...cstr("\\*")];
const wc = 14 + 1;
const bytesStart = 32 + 1 + wc * 2 + 2;
const paramOff = bytesStart + 3;
const words = [
...u16(t2params.length), ...u16(0), ...u16(100), ...u16(8000),
1, 0, ...u16(0), ...u32(0), ...u16(0),
...u16(t2params.length), ...u16(paramOff),
...u16(0), ...u16(0),
1, 0, ...u16(1)
];
const bytes = [0, 0, 0, ...t2params];
const reply = session.handle(smbReq(CMD_TRANSACTION2, words, bytes, 1, 1))!;
const parsed = parseSmb(reply)!;
ok(parsed.status === 0, "status OK");
// Walk the data block via NextEntryOffset and verify the long name appears
// intact and the chain terminates with 0.
const rw = parsed.words;
const dOff = (rw[14] | (rw[15] << 8)) - (32 + 1 + parsed.wordCount * 2 + 2);
const dLen = rw[12] | (rw[13] << 8);
const data = parsed.bytes.slice(dOff, dOff + dLen);
const names: string[] = [];
let off = 0;
for (;;) {
const next = data[off] | (data[off+1]<<8) | (data[off+2]<<16) | (data[off+3]<<24);
const fnLen = data[off+60] | (data[off+61]<<8);
// FileNameLength counts the trailing null (Samba/win9x compat)
names.push(String.fromCharCode(...data.slice(off+94, off+94+fnLen)).replace(/\0$/, ""));
if (next === 0) break;
off += next;
}
ok(names.includes("A Long Filename Here.txt"), `LFN intact: ${JSON.stringify(names)}`);
ok(names.includes(".") && names.includes(".."), "dot entries present");
}
// ─── Test 5d: filename safety + hidden attrs ─────────────────────────────────
console.log("\n[5d] filename safety");
{
const hz = path.join(tmpRoot, "hazard");
fs.mkdirSync(hz);
for (const n of ["con.txt", "aux", "nul.tar.gz", ".DS_Store", ".secret", "trail. "])
fs.writeFileSync(path.join(hz, n), "x");
const t2params = [...u16(0x16), ...u16(100), ...u16(0), ...u16(0x104),
...u32(0), ...cstr("\\hazard\\*")];
const wc = 14 + 1;
const bytesStart = 32 + 1 + wc * 2 + 2;
const paramOff = bytesStart + 3;
const words = [
...u16(t2params.length), ...u16(0), ...u16(100), ...u16(8000),
1, 0, ...u16(0), ...u32(0), ...u16(0),
...u16(t2params.length), ...u16(paramOff),
...u16(0), ...u16(0),
1, 0, ...u16(1)
];
const reply = session.handle(smbReq(CMD_TRANSACTION2, words,
[0, 0, 0, ...t2params], 1, 1))!;
const parsed = parseSmb(reply)!;
const rw = parsed.words;
const dOff = (rw[14] | (rw[15] << 8)) - (32 + 1 + parsed.wordCount * 2 + 2);
const dLen = rw[12] | (rw[13] << 8);
const data = parsed.bytes.slice(dOff, dOff + dLen);
const ents = new Map<string, number>();
for (let off = 0;;) {
const next = data[off] | (data[off+1]<<8) | (data[off+2]<<16) | (data[off+3]<<24);
const attr = data[off+56] | (data[off+57]<<8);
const fnLen = data[off+60] | (data[off+61]<<8);
const nm = String.fromCharCode(...data.slice(off+94, off+94+fnLen)).replace(/\0$/, "");
ents.set(nm, attr);
if (next === 0) break;
off += next;
}
const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
const bad = [...ents.keys()].filter(n => reserved.test(n.split(".")[0]));
ok(bad.length === 0, `no reserved basenames: ${JSON.stringify([...ents.keys()])}`);
ok(ents.has("con_.txt") && ents.has("aux_"), "reserved names suffixed");
ok(ents.has("nul_.tar.gz"), "reserved across multi-ext");
ok(ents.has("trail__"), "trailing dot/space replaced");
ok((ents.get(".DS_Store")! & 0x06) === 0x06, ".DS_Store hidden+system");
ok((ents.get(".secret")! & 0x02) === 0x02, "dotfile hidden");
ok((ents.get("con_.txt")! & 0x02) === 0, "regular file not hidden");
}
// ─── Test 5c: RAP NetShareEnum lists user share + TOOLS ──────────────────────
console.log("\n[5c] RAP NetShareEnum");
{
// TREE_CONNECT IPC$ first
const ipc = parseSmb(session.handle(smbReq(CMD_TREE_CONNECT_ANDX,
[0xff,0,0,0,...u16(0),...u16(1)],
[0, ...cstr("\\\\HOST\\IPC$"), ...cstr("IPC")], 0, 1))!)!;
ok(ipc.tid === 0xfffe, `IPC$ tid=${ipc.tid}`);
// RAP NetShareEnum: TRANS over \PIPE\LANMAN
const rap = [...u16(0), ...cstr("WrLeh"), ...cstr("B13BWz"), ...u16(1), ...u16(4096)];
const wc = 14;
const bytesStart = 32 + 1 + wc * 2 + 2;
const name = cstr("\\PIPE\\LANMAN");
const paramOff = bytesStart + name.length;
const words = [
...u16(rap.length), ...u16(0), ...u16(100), ...u16(4096),
0, 0, ...u16(0), ...u32(0), ...u16(0),
...u16(rap.length), ...u16(paramOff),
...u16(0), ...u16(0),
0, 0
];
const reply = session.handle(smbReq(0x25, words, [...name, ...rap], ipc.tid, 1))!;
const dataStr = String.fromCharCode(...parseSmb(reply)!.bytes);
const userShare = path.basename(tmpRoot).replace(/[^A-Za-z0-9_$~!#%&'()@^`{}.-]/g, "")
.toUpperCase().slice(0, 12);
ok(dataStr.includes("TOOLS"), "TOOLS share listed");
ok(dataStr.includes(userShare), `user share "${userShare}" listed`);
}
// ─── Test 6: OPEN + READ + CLOSE ─────────────────────────────────────────────
console.log("\n[6] OPEN_ANDX + READ_ANDX + CLOSE");
let openedFid = 0;
@@ -243,21 +367,61 @@ console.log("\n[7] Error handling");
ok(parsed.status !== 0, "lexical traversal (../) blocked");
}
{
// Virtual file: open and read _MAPZ.BAT
// Virtual file: connect to TOOLS share, open and read _MAPZ.BAT
const tcReq = smbReq(CMD_TREE_CONNECT_ANDX,
[0xff, 0, 0, 0, ...u16(0), ...u16(1)],
[0, ...cstr("\\\\192.168.86.1\\TOOLS"), ...cstr("?????")], 0, 1);
const tcParsed = parseSmb(session.handle(tcReq)!)!;
ok(tcParsed.tid === 2, `TOOLS share tid=${tcParsed.tid}`);
const oReq = smbReq(CMD_OPEN_ANDX,
[0xff,0,0,0,...u16(0),...u16(0),...u16(0),...u16(0),...u32(0),...u16(1),...u32(0),...u32(0),...u32(0)],
[...cstr("\\_MAPZ.BAT")], 1, 1);
[...cstr("\\_MAPZ.BAT")], tcParsed.tid, 1);
const oReply = session.handle(oReq)!;
const oParsed = parseSmb(oReply)!;
ok(oParsed.status === 0, "open virtual _MAPZ.BAT");
const vfid = oParsed.words[4] | (oParsed.words[5] << 8);
const rReq = smbReq(CMD_READ_ANDX,
[0xff,0,0,0,...u16(vfid),...u32(0),...u16(500),...u16(0),...u32(0),...u16(0)], [], 1, 1);
[0xff,0,0,0,...u16(vfid),...u32(0),...u16(500),...u16(0),...u32(0),...u16(0)], [], tcParsed.tid, 1);
const rReply = session.handle(rReq)!;
const rParsed = parseSmb(rReply)!;
const len = rParsed.words[10] | (rParsed.words[11] << 8);
const text = String.fromCharCode(...rParsed.bytes.slice(1, 1 + len));
ok(text.includes("NET USE Z:"), `virtual read: ${JSON.stringify(text.slice(0, 40))}`);
// SEEK to end → file size, then core READ (0x0a). This is the exact path
// Win95+Notepad take under NT LM 0.12 with Capabilities=0.
const oReq2 = smbReq(CMD_OPEN_ANDX,
[0xff,0,0,0,...u16(0),...u16(0),...u16(0),...u16(0),...u32(0),...u16(1),...u32(0),...u32(0),...u32(0)],
[...cstr("\\README.TXT")], tcParsed.tid, 1);
const oP2 = parseSmb(session.handle(oReq2)!)!;
const fid2 = oP2.words[4] | (oP2.words[5] << 8);
ok(oP2.status === 0 && fid2 > 0, `open README.TXT fid=${fid2}`);
const sP = parseSmb(session.handle(smbReq(0x12,
[...u16(fid2), ...u16(2), ...u32(0)], [], tcParsed.tid, 1))!)!;
const seekPos = sP.words[0] | (sP.words[1] << 8) | (sP.words[2] << 16) | (sP.words[3] << 24);
ok(sP.status === 0 && seekPos > 100, `SEEK end → size=${seekPos}`);
const rP = parseSmb(session.handle(smbReq(0x0a,
[...u16(fid2), ...u16(seekPos), ...u32(0), ...u16(seekPos)], [], tcParsed.tid, 1))!)!;
ok(rP.status === 0, "core READ status OK");
// bytes: 0x01 + len(2) + data
const dlen = rP.bytes[1] | (rP.bytes[2] << 8);
const body = String.fromCharCode(...rP.bytes.slice(3, 3 + dlen));
ok(dlen === seekPos, `core READ returned ${dlen} bytes`);
ok(body.includes("windows95 tools"), `README content: ${JSON.stringify(body.slice(0, 30))}`);
// READ_RAW (0x1a): response is raw bytes, no SMB header.
const raw = session.handle(smbReq(0x1a,
[...u16(fid2), ...u32(0), ...u16(65535), ...u16(0), ...u32(0), ...u16(0)],
[], tcParsed.tid, 1))!;
ok(raw.length === seekPos && raw[0] === 0x77 /* 'w' */,
`READ_RAW returned ${raw.length} raw bytes (no SMB header)`);
const rawBad = session.handle(smbReq(0x1a,
[...u16(0x7777), ...u32(0), ...u16(100), ...u16(0), ...u32(0), ...u16(0)],
[], tcParsed.tid, 1))!;
ok(rawBad.length === 0, "READ_RAW bad fid → 0-byte reply");
}
{
// symlink escape: link inside share → file outside share

View File

@@ -45,6 +45,13 @@ export class Writer {
this.chunks.push(0, 0);
return this;
}
patch32(at: number, v: number) {
this.chunks[at] = v & 0xff;
this.chunks[at+1] = (v >>> 8) & 0xff;
this.chunks[at+2] = (v >>> 16) & 0xff;
this.chunks[at+3] = (v >>> 24) & 0xff;
return this;
}
get length() { return this.chunks.length; }
build() { return new Uint8Array(this.chunks); }
}

1
src/renderer/styles.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module "*.css";

View File

@@ -0,0 +1,50 @@
import * as fs from "fs";
/**
* v86 disk buffer backed by synchronous fs reads.
*
* v86's stock async loaders (AsyncXHRBuffer / AsyncFileBuffer) return from
* .get() immediately and resolve the data on a later event-loop turn. For an
* ATAPI PIO READ(10) that means atapi_read() leaves the drive in BSY while the
* emulated CPU keeps running. Win95's ESDI_506/CDVSD path checks status twice
* after pushing the CDB, sees BSY both times, and issues DEVICE RESET (08h) —
* which cancels the in-flight read. Net effect: D: shows up but the volume
* never mounts. Serving the bytes synchronously closes that window.
*
* The hard disk doesn't hit this because ESDI_506 drives it via bus-master
* DMA, which is purely IRQ-driven on the host side.
*/
export class SyncFileBuffer {
public byteLength: number;
public onload: undefined | ((e: { buffer?: ArrayBuffer }) => void);
public onprogress: undefined | (() => void);
private fd: number;
constructor(path: string) {
this.fd = fs.openSync(path, "r");
this.byteLength = fs.fstatSync(this.fd).size;
this.onload = undefined;
this.onprogress = undefined;
}
load() {
this.onload?.({});
}
get(start: number, len: number, fn: (data: Uint8Array) => void) {
const buf = Buffer.alloc(len);
fs.readSync(this.fd, buf, 0, len, start);
fn(new Uint8Array(buf.buffer, buf.byteOffset, len));
}
set(_start: number, _slice: Uint8Array, fn: () => void) {
fn();
}
get_state() {
return [[]];
}
set_state(_state: unknown) {}
}

View File

@@ -0,0 +1,132 @@
import * as fs from "fs";
import * as path from "path";
/**
* Minimal read-only FAT32 walker. Just enough to pull user files out of the
* recovered overlay+base view — no writes, no FAT12/16, no exFAT.
*
* `readSector(lba)` returns 512 bytes at absolute LBA from the *full disk*
* (MBR at LBA 0). `isDirty(lba)` reports whether that sector came from the
* guest's write overlay; we use it to skip files the user never touched so
* the output isn't 200 MB of possibly-mismatched OS binaries.
*/
export async function extractFat32(
readSector: (lba: number) => Buffer,
isDirty: (lba: number) => boolean,
outDir: string,
): Promise<number> {
// First partition from the MBR.
const mbr = readSector(0);
const partLba = mbr.readUInt32LE(0x1be + 8);
const bpb = readSector(partLba);
const bytesPerSec = bpb.readUInt16LE(11);
const secPerClus = bpb.readUInt8(13);
const rsvd = bpb.readUInt16LE(14);
const nFats = bpb.readUInt8(16);
const secPerFat = bpb.readUInt32LE(36);
const rootClus = bpb.readUInt32LE(44);
if (bytesPerSec !== 512) throw new Error("unexpected sector size");
const fatLba = partLba + rsvd;
const dataLba = partLba + rsvd + nFats * secPerFat;
const clusLba = (c: number) => dataLba + (c - 2) * secPerClus;
const fatSecCache = new Map<number, Buffer>();
const nextCluster = (c: number) => {
const off = c * 4;
const sec = fatLba + (off >> 9);
let b = fatSecCache.get(sec);
if (!b) fatSecCache.set(sec, (b = readSector(sec)));
return b.readUInt32LE(off & 511) & 0x0fffffff;
};
const chain = (c: number) => {
const out: number[] = [];
while (c >= 2 && c < 0x0ffffff8 && out.length < 1 << 20) {
out.push(c);
c = nextCluster(c);
}
return out;
};
const readClusters = (clusters: number[]) => {
const buf = Buffer.allocUnsafe(clusters.length * secPerClus * 512);
let o = 0;
for (const c of clusters) {
const base = clusLba(c);
for (let s = 0; s < secPerClus; s++)
readSector(base + s).copy(buf, o + s * 512);
o += secPerClus * 512;
}
return buf;
};
const anyDirty = (clusters: number[]) => {
for (const c of clusters) {
const base = clusLba(c);
for (let s = 0; s < secPerClus; s++) if (isDirty(base + s)) return true;
}
return false;
};
const safe = (n: string) =>
n.replace(/[\\/:*?"<>|]/g, "_").replace(/[. ]+$/, "") || "_";
let files = 0;
const walk = async (clus: number, hostDir: string) => {
const raw = readClusters(chain(clus));
let lfn = "";
for (let i = 0; i + 32 <= raw.length; i += 32) {
const e = raw.subarray(i, i + 32);
if (e[0] === 0) break;
if (e[0] === 0xe5) {
lfn = "";
continue;
}
const attr = e[11];
if ((attr & 0x3f) === 0x0f) {
// VFAT LFN entries arrive last-first; each carries 13 UCS-2 chars.
let part = "";
for (const o of [1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30]) {
const ch = e.readUInt16LE(o);
if (ch === 0 || ch === 0xffff) break;
part += String.fromCharCode(ch);
}
lfn = part + lfn;
continue;
}
if (attr & 0x08) {
lfn = "";
continue; // volume label
}
const short =
e.toString("latin1", 0, 8).trimEnd() +
(e[8] !== 0x20
? "." + e.toString("latin1", 8, 11).trimEnd()
: "");
const name = lfn || short;
lfn = "";
if (name === "." || name === "..") continue;
const start = (e.readUInt16LE(20) << 16) | e.readUInt16LE(26);
if (attr & 0x10) {
if (start >= 2) await walk(start, path.join(hostDir, safe(name)));
} else {
const size = e.readUInt32LE(28);
if (size === 0 || start < 2) continue;
const cl = chain(start);
if (!anyDirty(cl)) continue;
await fs.promises.mkdir(hostDir, { recursive: true });
await fs.promises.writeFile(
path.join(hostDir, safe(name)),
readClusters(cl).subarray(0, size),
);
files++;
}
}
};
await fs.promises.mkdir(outDir, { recursive: true });
await walk(rootClus, outDir);
return files;
}

View File

@@ -11,3 +11,7 @@ export async function getStatePath(): Promise<string> {
const statePath = await ipcRenderer.invoke(IPC_COMMANDS.GET_STATE_PATH);
return (_statePath = statePath);
}
export function getLegacyStatePath(): Promise<string | null> {
return ipcRenderer.invoke(IPC_COMMANDS.GET_LEGACY_STATE_PATH);
}

View File

@@ -0,0 +1,88 @@
import * as fs from "fs";
import * as path from "path";
import { CONSTANTS } from "../../constants";
import { getDiskImageSize } from "../../utils/disk-image-size";
import { extractFat32 } from "./fat32-extract";
declare const V86: any;
/**
* Reconstruct the user's old C:\ from a legacy state-vN.bin and extract any
* file the guest ever wrote to a host folder — without booting Windows.
*
* v86's async-hda buffer serialises every 256-byte block the guest *wrote*
* (libv86 xa.get_state). We spin up a throwaway v86 (autostart:false),
* restore the legacy state to populate that block cache, then walk the
* FAT32 tree reading each sector as overlay-if-present-else-base.
*
* The base supplies the partition table / BPB (which Windows only reads,
* never writes) — see STATE_VERSION in constants.ts for the geometry
* constraint that keeps that valid across releases.
*/
export async function recoverLegacyDisk(
legacyStatePath: string,
outDir: string,
): Promise<{ dir: string; files: number }> {
const emulator = new V86({
wasm_path: path.join(__dirname, "build/v86.wasm"),
memory_size: 128 * 1024 * 1024,
vga_memory_size: 64 * 1024 * 1024,
bios: { url: path.join(__dirname, "../../bios/seabios.bin") },
vga_bios: { url: path.join(__dirname, "../../bios/vgabios.bin") },
hda: {
url: CONSTANTS.IMAGE_PATH,
async: true,
size: await getDiskImageSize(CONSTANTS.IMAGE_PATH),
},
autostart: false,
disable_keyboard: true,
disable_mouse: true,
disable_speaker: true,
});
await new Promise<void>((resolve) =>
emulator.add_listener("emulator-loaded", resolve),
);
let files = 0;
const baseFd = fs.openSync(CONSTANTS.IMAGE_PATH, "r");
try {
const state = fs.readFileSync(legacyStatePath);
await emulator.restore_state(state.buffer);
const buf = emulator.v86?.cpu?.devices?.ide?.primary?.master?.buffer as {
block_cache: Map<number, Uint8Array>;
block_cache_is_write: Set<number>;
};
if (!buf?.block_cache) {
throw new Error("hda block cache not reachable after restore");
}
// v86 caches in 256-byte blocks; FAT works in 512-byte sectors.
const sec = Buffer.allocUnsafe(512);
const readSector = (lba: number) => {
const lo = buf.block_cache_is_write.has(lba * 2)
? buf.block_cache.get(lba * 2)
: undefined;
const hi = buf.block_cache_is_write.has(lba * 2 + 1)
? buf.block_cache.get(lba * 2 + 1)
: undefined;
if (lo && hi) return Buffer.concat([lo, hi]);
fs.readSync(baseFd, sec, 0, 512, lba * 512);
if (lo) sec.set(lo, 0);
if (hi) sec.set(hi, 256);
return Buffer.from(sec);
};
const isDirty = (lba: number) =>
buf.block_cache_is_write.has(lba * 2) ||
buf.block_cache_is_write.has(lba * 2 + 1);
files = await extractFat32(readSector, isDirty, outDir);
} finally {
fs.closeSync(baseFd);
await emulator.destroy();
}
return { dir: outDir, files };
}

View File

@@ -5,12 +5,11 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>windows95</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../src/less/vendor/98.css">
<link rel="stylesheet" href="../src/less/root.less">
<link rel="stylesheet" href="../renderer.css">
<!-- libv86 -->
</head>
<body class="paused windows95">
<div id="app"></div>
<script src="../src/renderer/app.tsx"></script>
<script src="../renderer.js"></script>
</body>
</html>

View File

@@ -11,7 +11,7 @@
<hr>
<p>I've installed a few apps and games for you to try out. Check out the Games folder on the desktop!</p>
<p>If you want to try other games, I recommend trying to find them on the Internet Archive. On your host computer, visit https://archive.org, then find the "Classic PC Games" category. Once downloaded, you can import them into windows95 from <a href="http://my-computer">your host's Download folder</a>.</p>
<p>If you want to try other games, I recommend trying to find them on the Internet Archive. On your host computer, visit https://archive.org, then find the "Classic PC Games" category. Once downloaded, you can import them into windows95 from drive <b>Z:</b> in <b>My Computer</b> &mdash; that's the shared folder from your host machine.</p>
</font>
</td>
</tr>

View File

@@ -17,18 +17,18 @@
<font face="Arial" color="#000000">
<p>Hi, I'm Felix, the maker behind windows95. I hope you're having fun!</p>
<p>Reach out to me in a modern browser (as in: not in windows95) on <font color="#0000FF">felixrieseberg.com</font> or find me on Bluesky at <font color="#0000FF">@felixrieseberg</font>.</p>
<p>Reach out to me in a modern browser (as in: not in windows95) on <font color="#0000FF">felixrieseberg.com</font> or find me on Twitter at <font color="#0000FF">@felixrieseberg</font>.</p>
<hr width="75%">
<a name="internet"></a>
<font size="5" color="#000000"><img src="images/ie.gif" width="16" height="16" border="0" align="absmiddle"> <b>The Internet!</b></font>
<hr width="75%">
<p>In a major update since the last version, windows95 now has working Internet! That said, most modern websites will not work, so brace yourself. I recommend using <a href="http://theoldnet.com/" target="_blank">The Old Net</a> to travel back in time.</p>
<p>windows95 has working Internet! That said, most modern websites will not work, so brace yourself. I recommend using <a href="http://theoldnet.com/" target="_blank">The Old Net</a> to travel back in time.</p>
</font>
<center>
<font size="2" color="#000000">Last updated: 2025</font>
<font size="2" color="#000000">Last updated: 2026</font>
</center>
</td>
</tr>

View File

@@ -11,13 +11,24 @@ async function main() {
let failed = false
for (const link of links) {
// Release download URLs are chicken-and-egg: README is updated to point at
// the new version before the release build that creates those assets has
// run (and lint gates that build). Skip them.
if (/\/releases\/download\//.test(link)) {
console.log(`⏭️ ${link} (release asset, skipped)`)
continue
}
try {
const response = await fetch(link, { method: 'HEAD' })
if (!response.ok) {
// If we're inside GitHub's release asset server, we just ran into AWS not allowing
// HEAD requests, which is different from a 404.
if (!response.url.startsWith('https://github-production-release-asset')) {
// GitHub's release-asset and user-attachments CDNs reject anonymous HEAD
// requests (403), which is different from a 404.
const isGithubCdn =
response.url.startsWith('https://github-production-release-asset') ||
response.url.startsWith('https://github-production-user-asset') ||
link.startsWith('https://github.com/user-attachments/')
if (!isGithubCdn) {
throw new Error (`HTTP Error Response: ${response.status} ${response.statusText}`)
}
}

View File

@@ -1,11 +1,36 @@
mkdir images
cd images
$ErrorActionPreference = "Stop"
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($env:DISK_URL, "$(Resolve-Path .)\images.zip")
# Pulls the disk image from a private GitHub release.
# Requires DISK_REPO, DISK_TAG, GH_TOKEN.
if (-not $env:DISK_REPO) { Write-Host "::error::DISK_REPO not set"; exit 1 }
if (-not $env:DISK_TAG) { Write-Host "::error::DISK_TAG not set"; exit 1 }
if (-not $env:GH_TOKEN) { Write-Host "::error::GH_TOKEN not set"; exit 1 }
New-Item -ItemType Directory -Force -Path images | Out-Null
Set-Location images
gh release download $env:DISK_TAG -R $env:DISK_REPO -p '*.zip' -O images.zip --clobber
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
7z t images.zip | Out-Null
if ($LASTEXITCODE -ne 0) {
$size = (Get-Item images.zip).Length
Write-Host "::error::Downloaded file is not a valid zip (size: $size bytes)."
exit 1
}
7z x images.zip -y -aoa
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Remove-Item images.zip
Remove-Item __MACOSX -Recurse -ErrorAction Ignore
cd ..
Tree ./ /F
Set-Location ..
if (-not (Test-Path images/windows95.img)) {
Write-Host "::error::images/windows95.img not found after extraction"
Get-ChildItem images
exit 1
}
Get-ChildItem images

35
tools/download-disk.sh Normal file → Executable file
View File

@@ -1,10 +1,35 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
set -euo pipefail
# Pulls the disk image from a private GitHub release.
# Requires:
# DISK_REPO - e.g. felixrieseberg/windows95-images
# DISK_TAG - e.g. v5
# GH_TOKEN - a token with read access to DISK_REPO (set by the workflow)
: "${DISK_REPO:?DISK_REPO not set}"
: "${DISK_TAG:?DISK_TAG not set}"
: "${GH_TOKEN:?GH_TOKEN not set}"
mkdir -p ./images
cd ./images
wget -O images.zip $DISK_URL
gh release download "$DISK_TAG" -R "$DISK_REPO" -p '*.zip' -O images.zip --clobber
if ! unzip -tq images.zip > /dev/null; then
echo "::error::Downloaded file is not a valid zip (size: $(wc -c < images.zip) bytes)."
exit 1
fi
unzip -o images.zip
rm images.zip
rm -r __MACOSX
rm -f images.zip
rm -rf __MACOSX
cd -
ls images
if [ ! -f images/windows95.img ]; then
echo "::error::images/windows95.img not found after extraction"
ls -la images/
exit 1
fi
ls -la images/

View File

@@ -1,7 +1,7 @@
/* tslint:disable */
const { compileParcel } = require('./parcel-build')
const { compileVite } = require('./vite-build')
module.exports = async () => {
await Promise.all([compileParcel()])
await compileVite()
}

9
tools/pack-disk.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env sh
# Produce the images zip that CI's DISK_URL secret should point at.
# Inverse of download-disk.sh: flat archive of windows95.img + default-state.bin.
set -e
OUT="${1:-images_$(date +%Y%m%d).zip}"
cd images
zip -9 "../$OUT" windows95.img default-state.bin
cd -
ls -lh "$OUT"

View File

@@ -1,91 +0,0 @@
/* tslint:disable */
const Bundler = require('parcel-bundler')
const path = require('path')
const fs = require('fs')
// libv86 checks `typeof module.exports` before `typeof window` when deciding
// where to export V86. In an Electron renderer with nodeIntegration both exist,
// so it ends up on module.exports instead of window. This shim copies it over.
const LIBV86_SHIM = `<script src="libv86.js"></script>
<script>if (typeof module !== "undefined" && module.exports && module.exports.V86) window.V86 = module.exports.V86;</script>`
// v86's node-path file loader used `await import("node:...")` until d4c5fa86
// switched it to require(). Dynamic import of node: URLs doesn't work in an
// Electron renderer — only require() does. The literals are stable across
// Closure builds; if they're absent the build is post-d4c5fa86 and already
// uses require, so a no-op is correct.
const V86_NODE_IMPORTS = [
['await import("node:fs/promises")', 'require("fs").promises'],
['await import("node:"+"fs/promises")', 'require("fs").promises'],
['await import("node:crypto")', 'require("crypto")'],
];
async function copyLib() {
const target = path.join(__dirname, '../dist/static')
const lib = path.join(__dirname, '../src/renderer/lib')
const index = path.join(target, 'index.html')
await fs.promises.cp(lib, target, { recursive: true });
const libv86path = path.join(target, 'libv86.js')
let libv86 = fs.readFileSync(libv86path, 'utf-8')
let patchCount = 0;
for (const [from, to] of V86_NODE_IMPORTS) {
const next = libv86.split(from).join(to);
if (next !== libv86) { patchCount++; libv86 = next; }
}
if (patchCount > 0) {
fs.writeFileSync(libv86path, libv86)
console.log(`libv86: ${patchCount} dynamic-import → require`)
}
const indexContents = fs.readFileSync(index, 'utf-8');
const replacedContents = indexContents.replace('<!-- libv86 -->', LIBV86_SHIM)
fs.writeFileSync(index, replacedContents)
}
async function compileParcel (options = {}) {
const entryFiles = [
path.join(__dirname, '../static/index.html'),
path.join(__dirname, '../src/main/main.ts')
]
const bundlerOptions = {
outDir: './dist', // The out directory to put the build files in, defaults to dist
outFile: undefined, // The name of the outputFile
publicUrl: '../', // The url to server on, defaults to dist
watch: false, // whether to watch the files and rebuild them on change, defaults to process.env.NODE_ENV !== 'production'
cache: false, // Enabled or disables caching, defaults to true
cacheDir: '.cache', // The directory cache gets put in, defaults to .cache
contentHash: false, // Disable content hash from being included on the filename
minify: false, // Minify files, enabled if process.env.NODE_ENV === 'production'
scopeHoist: false, // turn on experimental scope hoisting/tree shaking flag, for smaller production bundles
target: 'electron', // browser/node/electron, defaults to browser
// https: { // Define a custom {key, cert} pair, use true to generate one or false to use http
// cert: './ssl/c.crt', // path to custom certificate
// key: './ssl/k.key' // path to custom key
// },
logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
hmr: false, // Enable or disable HMR while watching
hmrPort: 0, // The port the HMR socket runs on, defaults to a random free port (0 in node.js resolves to a random free port)
sourceMaps: false, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
hmrHostname: '', // A hostname for hot module reload, default to ''
detailedReport: false, // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled,
...options
}
const bundler = new Bundler(entryFiles, bundlerOptions)
// Run the bundler, this returns the main bundle
// Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild
await bundler.bundle()
await copyLib();
}
module.exports = {
compileParcel
}
if (require.main === module) compileParcel()

View File

@@ -1,11 +0,0 @@
const { compileParcel } = require('./parcel-build')
async function watchParcel () {
return compileParcel({ watch: true })
}
module.exports = {
watchParcel
}
if (require.main === module) watchParcel()

View File

@@ -20,10 +20,10 @@ rm -f "$STATUS" "$DONE" "$SCREEN"
pkill -f "windows95/node_modules/electron" 2>/dev/null || true
sleep 1
# build (parcel only — forge's generateAssets does this too but we want
# build (vite only — forge's generateAssets does this too but we want
# direct control without the forge startup overhead)
rm -rf dist .cache
node tools/parcel-build.js > /tmp/win95-build.log 2>&1
rm -rf dist
node tools/vite-build.js > /tmp/win95-build.log 2>&1
if [ $? -ne 0 ]; then
echo "BUILD FAILED"
tail -20 /tmp/win95-build.log

65
tools/probe-tcp.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Probe the tcp-relay recv() path: boot Win95, telnet to a fake upstream
# on port 7777, and dump the per-frame trace. PASS = guest ACKs the async
# banner (i.e., recv() returned).
set -e
cd "$(dirname "$0")/.."
PORT=7777
TRACE=/tmp/win95-tcp-trace.log
RELAY="${TMPDIR:-/tmp}/win95-tcp-relay.log"
STATUS=/tmp/win95-probe.json
TIMEOUT=${TIMEOUT:-90}
pkill -9 -f "windows95.*electron" 2>/dev/null || true
sleep 1
rm -f "$HOME/Library/Application Support/windows95/state-v4.bin"
rm -f "$STATUS" /tmp/win95-probe.done /tmp/win95-screen.png "$TRACE" "$RELAY"
rm -rf dist
node tools/vite-build.js > /tmp/win95-build.log 2>&1 || {
echo "BUILD FAILED"; tail -30 /tmp/win95-build.log; exit 1
}
WIN95_PROBE=1 \
WIN95_PROBE_RUN="${RUN:-ping -t 8.8.8.8}" \
WIN95_PROBE_RUN2="${RUN2:-telnet 1.1.1.1 $PORT}" \
WIN95_PROBE_RUN2_WAIT="${RUN2_WAIT:-3000}" \
WIN95_PROBE_RUN_AFTER="${RUN_AFTER:-}" \
WIN95_PROBE_RUN_WAIT="${RUN_WAIT:-6000}" \
WIN95_TCP_TEST_PORT=$PORT \
WIN95_TCP_TEST_MODE="${MODE:-banner}" \
WIN95_TCP_TEST_DELAY="${DELAY:-50}" \
WIN95_TCP_TEST_BYTES="${BYTES:-3000}" \
WIN95_TCP_TRACE=$PORT \
WIN95_SMB_SHARE="$HOME/Downloads" \
./node_modules/.bin/electron . > /tmp/win95-electron.log 2>&1 &
PID=$!
echo "electron pid=$PID, waiting up to ${TIMEOUT}s…"
VERDICT=TIMEOUT
for i in $(seq 1 "$TIMEOUT"); do
kill -0 $PID 2>/dev/null || { VERDICT=CRASHED; break; }
if [ -f /tmp/win95-probe.done ] && grep -q FAIL /tmp/win95-probe.done; then
VERDICT="BOOT_$(cat /tmp/win95-probe.done)"; break
fi
if [ -f "$TRACE" ] && grep -q '→ guest .* banner' "$RELAY" 2>/dev/null; then
# Banner was written; give guest 8 s to ACK, then decide.
sleep 8
if grep -Eq 'guest→.* ack=(279[8-9]|2[89][0-9]{2}|[3-9][0-9]{3}|[1-9][0-9]{4,}) ' "$TRACE"; then
VERDICT=PASS
else
VERDICT=FAIL
fi
break
fi
sleep 1
done
kill $PID 2>/dev/null || true
wait $PID 2>/dev/null || true
echo "─── relay ───"; [ -f "$RELAY" ] && cat "$RELAY"
echo "─── trace ───"; [ -f "$TRACE" ] && cat "$TRACE"
echo "═══ $VERDICT ═══"
[ "$VERDICT" = PASS ]

View File

@@ -1,189 +1,166 @@
#!/usr/bin/env node
/**
* Updates v86 by building the wasm from a local checkout. The libv86.js +
* v86.wasm pair MUST be ABI-matched — copy.sh historically rebuilds the JS
* without rebuilding the wasm, and a mismatch silently breaks fresh boot
* (state restore still works because the CPU snapshot is opaque, so you
* won't notice until Win95 BSODs at the splash screen with "Invalid VxD
* dynamic link call").
* Build and install v86 (wasm + libv86.js + BIOS) from a local checkout.
*
* Usage:
* node tools/update-v86.js [path/to/v86] # builds wasm from source
* node tools/update-v86.js --js-only # just download libv86.js
* node tools/update-v86.js [path/to/v86]
*
* The wasm build needs `rustup target add wasm32-unknown-unknown` and clang.
* libv86.js needs Java + Closure; if you don't have those, --js-only fetches
* from copy.sh and warns if its Last-Modified is far from your wasm build.
* Defaults to ../v86 relative to this repo. Expects the checkout to be on
* `fork/windows95-base` (or a branch with both bug fixes applied):
*
* - `electron-renderer-fs-loader` — file loader uses require() instead of
* dynamic import (needed for Electron renderer, PR #1540)
* - `ide-shared-registers` — ATA Command Block register writes hit both
* master and slave, as the spec says they should (fixes Win95/98 boot
* on disks >535MiB, PR #1541)
*
* If either PR is merged into upstream, rebase windows95-base and drop it.
*
* Prereqs (all must be installed — no fallbacks):
* cargo + rustup target add wasm32-unknown-unknown
* clang
* java (e.g. brew install openjdk)
* <v86>/closure-compiler/compiler.jar (v20210601 — pinned by v86's Makefile)
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const { execSync } = require('child_process');
const { execFileSync } = require('child_process');
const LIB_DIR = path.join(__dirname, '../src/renderer/lib');
const V86_DIR = process.argv.find(a => a !== process.argv[0] && a !== process.argv[1] && !a.startsWith('--'))
|| path.resolve(__dirname, '../../v86');
const JS_ONLY = process.argv.includes('--js-only');
const SKEW_DAYS = 14;
const WINDOWS95_DIR = path.resolve(__dirname, '..');
const V86_DIR = process.argv[2]
? path.resolve(process.argv[2])
: path.resolve(WINDOWS95_DIR, '../v86');
function head(url) {
return new Promise((resolve, reject) => {
https.request(url, { method: 'HEAD' }, (res) => {
resolve({ status: res.statusCode, lastModified: res.headers['last-modified'] });
}).on('error', reject).end();
});
const LIB_DIR = path.join(WINDOWS95_DIR, 'src/renderer/lib');
const BIOS_DIR = path.join(WINDOWS95_DIR, 'bios');
const JAVA_BIN = '/opt/homebrew/opt/openjdk/bin/java';
function require_tool(cmd, desc) {
try {
execFileSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' });
} catch {
throw new Error(`Missing prerequisite: ${desc} (${cmd} not on PATH)`);
}
}
function download(url, dest) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode !== 200) return reject(new Error(`${url} → HTTP ${res.statusCode}`));
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
const buf = Buffer.concat(chunks);
fs.writeFileSync(dest, buf);
console.log(` ${path.basename(dest)}: ${(buf.length / 1024).toFixed(0)} KB`);
resolve(res.headers['last-modified']);
});
}).on('error', reject);
});
function run(cmd, args, opts = {}) {
execFileSync(cmd, args, { stdio: 'inherit', ...opts });
}
function check_prereqs() {
require_tool('cargo', 'rust/cargo');
require_tool('clang', 'clang');
// cargo needs the wasm32 target
const targets = execFileSync('rustup', ['target', 'list', '--installed']).toString();
if (!targets.includes('wasm32-unknown-unknown')) {
throw new Error('Missing rust target. Run: rustup target add wasm32-unknown-unknown');
}
// Java comes from homebrew openjdk on macOS — the v86 Makefile invokes `java`
// directly, so we have to put the homebrew java on PATH for its make calls
// (or install openjdk into the system). We check for an explicit binary so
// the error is clear.
if (!fs.existsSync(JAVA_BIN)) {
throw new Error(`Missing java at ${JAVA_BIN}. Install with: brew install openjdk`);
}
const closureJar = path.join(V86_DIR, 'closure-compiler', 'compiler.jar');
if (!fs.existsSync(closureJar)) {
throw new Error(
`Missing Closure compiler at ${closureJar}.\n` +
`Download v20210601 (pinned by v86's Makefile):\n` +
` mkdir -p ${path.dirname(closureJar)}\n` +
` curl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar -o ${closureJar}`
);
}
if (!fs.existsSync(path.join(V86_DIR, 'Makefile'))) {
throw new Error(`No v86 checkout at ${V86_DIR}. Pass a path as the first argument or clone copy/v86 there.`);
}
}
function build_v86() {
const env = { ...process.env, PATH: `/opt/homebrew/opt/openjdk/bin:${process.env.PATH}` };
console.log('Building v86.wasm…');
run('make', ['build/v86.wasm'], { cwd: V86_DIR, env });
console.log('Building libv86.js…');
run('make', ['build/libv86.js'], { cwd: V86_DIR, env });
}
function install() {
const copies = [
['build/v86.wasm', 'build/v86.wasm'],
['build/libv86.js', 'libv86.js'],
];
for (const [src, dest] of copies) {
fs.copyFileSync(path.join(V86_DIR, src), path.join(LIB_DIR, dest));
const size = fs.statSync(path.join(LIB_DIR, dest)).size;
console.log(` ${dest}: ${(size / 1024).toFixed(0)} KB`);
}
for (const bios of ['seabios.bin', 'vgabios.bin']) {
fs.copyFileSync(path.join(V86_DIR, 'bios', bios), path.join(BIOS_DIR, bios));
}
console.log(' seabios.bin + vgabios.bin');
}
/**
* v86 commit 1b90d2e7 (May 2025) changed ATA Command Block register writes
* to only target current_interface instead of both master and slave. Those
* registers (ports 0x1F1-0x1F6) are channel-shared per the ATA spec — both
* drives on the cable see the same register file. Win95's ESDI_506.PDR
* writes them, switches drive-select, expects them to still be there.
* Result: IDE IRQ never fires, splash screen hang.
*
* Found via JS-only bisect: prod wasm + freshly-built libv86.js, parent
* 3c944a02 boots, 1b90d2e7 hangs deterministically.
* Sanity check the installed files for the invariants our SMB integration
* and Electron renderer depend on. If any of these fail, v86 changed under us
* and src/renderer/smb/index.ts probably needs updating — see the README at
* src/renderer/smb/README.md for why.
*/
function patchIdeSharedRegisters(ideJsPath) {
let s = fs.readFileSync(ideJsPath, 'utf-8');
const re = /this\.current_interface\.(\w+_reg) = \(this\.current_interface\.\1 << 8 \| data\) & 0xFFFF;/g;
const matches = [...s.matchAll(re)];
if (matches.length === 0) {
console.log(' ide.js: shared-register patch already applied or upstream fixed it');
return;
function sanity_check() {
const js = fs.readFileSync(path.join(LIB_DIR, 'libv86.js'), 'utf-8');
const checks = [
// The electron-renderer-fs-loader fix: don't use dynamic import for fs
[!/await import\("node:/.test(js),
'libv86.js uses `await import("node:...")` — the Electron renderer fs loader PR was reverted?'],
// The ide-shared-registers fix: writes go to both master and slave
// (minified has no spaces: `this.master.features_reg=(this.master...`)
[/this\.master\.features_reg=\(this\.master\.features_reg/.test(js),
'libv86.js ide.js did not get the shared-register fix — is the windows95-base branch still in sync?'],
// Export pattern still shims the way vite-build expects
[js.includes('module.exports') && js.includes('window'),
'libv86.js export pattern changed — check the runtime shim in vite-build.js'],
// SMB integration needs the tcp-connection bus event (new API path in index.ts)
[js.includes('tcp-connection'),
'libv86.js no longer fires the tcp-connection bus event — SMB will fall back to the old-API theft hack'],
// Old-API fallback still present for defense in depth
[js.includes('on_tcp_connection'),
'libv86.js no longer has on_tcp_connection — harmless but surprising'],
];
let passed = 0;
for (const [ok, msg] of checks) {
if (ok) passed++;
else console.warn(' WARN:', msg);
}
if (matches.length < 5) {
throw new Error(`ide.js: expected ≥5 register write sites, found ${matches.length} — pattern changed`);
}
s = s.replace(re, (_, reg) =>
`this.master.${reg} = (this.master.${reg} << 8 | data) & 0xFFFF;\n` +
` this.slave.${reg} = (this.slave.${reg} << 8 | data) & 0xFFFF;`
);
fs.writeFileSync(ideJsPath, s);
console.log(` ide.js: restored shared-register writes (${matches.length} sites)`);
console.log(` sanity: ${passed}/${checks.length} checks passed`);
}
async function main() {
const jsDest = path.join(LIB_DIR, 'libv86.js');
const wasmDest = path.join(LIB_DIR, 'build/v86.wasm');
function main() {
console.log(`v86 checkout: ${V86_DIR}`);
const head = execFileSync('git', ['log', '-1', '--format=%h %s'], { cwd: V86_DIR }).toString().trim();
console.log(` ${head}`);
// ─── source patch (before any build) ─────────────────────────────────────
if (!JS_ONLY) {
const ideJs = path.join(V86_DIR, 'src/ide.js');
if (fs.existsSync(ideJs)) {
patchIdeSharedRegisters(ideJs);
}
}
// ─── wasm ────────────────────────────────────────────────────────────────
let wasmDate;
if (JS_ONLY) {
if (!fs.existsSync(wasmDest)) {
throw new Error(`--js-only requires an existing wasm at ${wasmDest}`);
}
wasmDate = fs.statSync(wasmDest).mtime;
console.log(`Keeping existing wasm (${wasmDate.toISOString().slice(0, 10)})`);
} else {
if (!fs.existsSync(path.join(V86_DIR, 'Makefile'))) {
throw new Error(`No v86 checkout at ${V86_DIR}. Clone copy/v86 there or pass a path.`);
}
const head = execSync('git log -1 --format="%h %ci"', { cwd: V86_DIR }).toString().trim();
console.log(`Building wasm from ${V86_DIR} @ ${head}`);
execSync('make build/v86.wasm', { cwd: V86_DIR, stdio: 'inherit' });
fs.copyFileSync(path.join(V86_DIR, 'build/v86.wasm'), wasmDest);
wasmDate = new Date();
console.log(` v86.wasm: ${(fs.statSync(wasmDest).size / 1024).toFixed(0)} KB`);
}
// ─── libv86.js ───────────────────────────────────────────────────────────
// Build from source if Closure is available; otherwise fetch and check skew.
const hasClosure = !JS_ONLY && fs.existsSync(path.join(V86_DIR, 'closure-compiler/compiler.jar'));
if (hasClosure) {
console.log('Building libv86.js (Closure)…');
execSync('make build/libv86.js', { cwd: V86_DIR, stdio: 'inherit' });
fs.copyFileSync(path.join(V86_DIR, 'build/libv86.js'), jsDest);
console.log(` libv86.js: ${(fs.statSync(jsDest).size / 1024).toFixed(0)} KB`);
} else {
console.log('No Closure jar — fetching libv86.js from copy.sh');
const lm = await download('https://copy.sh/v86/build/libv86.js', jsDest);
const jsDate = new Date(lm);
const skew = Math.abs(jsDate - wasmDate) / 86400000;
console.log(` JS: ${jsDate.toISOString().slice(0, 10)}`);
console.log(` wasm: ${wasmDate.toISOString().slice(0, 10)}`);
if (skew > SKEW_DAYS) {
throw new Error(
`JS and wasm are ${skew.toFixed(0)} days apart. ` +
`Either install Closure (java + v86/closure-compiler/compiler.jar) ` +
`to build libv86.js from the same commit, or git-checkout v86 to a ` +
`commit near ${jsDate.toISOString().slice(0, 10)} and rebuild the wasm.`
);
}
}
// ─── BIOS ────────────────────────────────────────────────────────────────
// SeaBIOS sets up the interrupt controller for whatever the emulated
// hardware presents. New v86 + old BIOS = APIC never armed = IDE IRQs
// never fire = boot hangs at the splash screen with no disk activity.
if (!JS_ONLY) {
const biosDir = path.join(__dirname, '../bios');
for (const f of ['seabios.bin', 'vgabios.bin']) {
fs.copyFileSync(path.join(V86_DIR, 'bios', f), path.join(biosDir, f));
console.log(` ${f}: ${(fs.statSync(path.join(biosDir, f)).size / 1024).toFixed(0)} KB`);
}
}
// ─── patch: phantom slave drive ──────────────────────────────────────────
// v86 bug since 1b90d2e7 (May 2025 IDE refactor): cpu.js does
// ide_config[0][1] = { buffer: settings.hdb }
// unconditionally inside the `if(settings.hda)` block. When hdb is
// undefined this creates a phantom 0-size HD on primary slave; Win95's
// ESDI_506.PDR detects it, sends IDENTIFY, and spins forever waiting for
// DRQ from a drive that has no sectors. State restore skips driver init,
// so it only bites on fresh boot.
//
// The pattern is structurally stable: `buffer` and `hdb` are option keys
// (externed, not mangled), `[0][1]=` is literal.
let js = fs.readFileSync(jsDest, 'utf-8');
const phantom = /(\w+)\[0\]\[1\]=\{buffer:(\w+)\.hdb\}/g;
const matches = [...js.matchAll(phantom)];
if (matches.length !== 1) {
throw new Error(
`phantom-slave patch: expected exactly 1 match, found ${matches.length}. ` +
`Either v86 fixed this upstream (good — remove this patch) or the ` +
`pattern changed. Check src/cpu.js around ide_config[0][1].`
);
}
js = js.replace(phantom, '$2.hdb&&($1[0][1]={buffer:$2.hdb})');
fs.writeFileSync(jsDest, js);
console.log(' patched: phantom slave drive guard (1 site)');
// ─── sanity ──────────────────────────────────────────────────────────────
if (!js.includes('process.versions.node'))
throw new Error('libv86 lost the process.versions.node check (file loader regression)');
if (!/this\.fetch=\([^)]*\)=>fetch\(/.test(js))
throw new Error('libv86 lost the fetch arrow wrapper');
if (!js.includes('window.V86=') && !js.includes('module.exports.V86='))
throw new Error('libv86 export pattern changed — check the runtime shim');
console.log('✓ installed (sanity checks pass)');
check_prereqs();
build_v86();
install();
sanity_check();
console.log('done');
}
main().catch((e) => { console.error('✗', e.message); process.exit(1); });
try { main(); }
catch (e) {
console.error('✗', e.message);
process.exit(1);
}

113
tools/vite-build.js Normal file
View File

@@ -0,0 +1,113 @@
const { build } = require('vite')
const { builtinModules } = require('module')
const path = require('path')
const fs = require('fs')
const root = path.join(__dirname, '..')
const nodeExternals = ['electron', ...builtinModules, ...builtinModules.map(m => `node:${m}`)]
// libv86 checks `typeof module.exports` before `typeof window` when deciding
// where to export V86. In an Electron renderer with nodeIntegration both exist,
// so it ends up on module.exports instead of window. This shim copies it over.
const LIBV86_SHIM = `<script src="libv86.js"></script>
<script>if (typeof module !== "undefined" && module.exports && module.exports.V86) window.V86 = module.exports.V86;</script>`
// v86's node-path file loader used `await import("node:...")` until d4c5fa86
// switched it to require(). Dynamic import of node: URLs doesn't work in an
// Electron renderer — only require() does. The literals are stable across
// Closure builds; if they're absent the build is post-d4c5fa86 and already
// uses require, so a no-op is correct.
const V86_NODE_IMPORTS = [
['await import("node:fs/promises")', 'require("fs").promises'],
['await import("node:"+"fs/promises")', 'require("fs").promises'],
['await import("node:crypto")', 'require("crypto")'],
];
async function copyLib() {
const target = path.join(root, 'dist/static')
const lib = path.join(root, 'src/renderer/lib')
const indexSrc = path.join(root, 'static/index.html')
const indexOut = path.join(target, 'index.html')
await fs.promises.cp(lib, target, { recursive: true });
const libv86path = path.join(target, 'libv86.js')
let libv86 = fs.readFileSync(libv86path, 'utf-8')
let patchCount = 0;
for (const [from, to] of V86_NODE_IMPORTS) {
const next = libv86.split(from).join(to);
if (next !== libv86) { patchCount++; libv86 = next; }
}
if (patchCount > 0) {
fs.writeFileSync(libv86path, libv86)
console.log(`libv86: ${patchCount} dynamic-import → require`)
}
const indexContents = fs.readFileSync(indexSrc, 'utf-8');
const replacedContents = indexContents.replace('<!-- libv86 -->', LIBV86_SHIM)
fs.writeFileSync(indexOut, replacedContents)
}
function mainConfig(watch) {
return {
root,
configFile: false,
build: {
outDir: 'dist/src/main',
emptyOutDir: false,
minify: false,
sourcemap: false,
target: 'node22',
lib: { entry: 'src/main/main.ts', formats: ['cjs'], fileName: () => 'main.js' },
rollupOptions: {
external: [...nodeExternals, 'electron-squirrel-startup', 'update-electron-app'],
},
watch: watch ? {} : undefined,
},
}
}
function rendererConfig(watch) {
return {
root,
configFile: false,
build: {
outDir: 'dist',
emptyOutDir: false,
minify: false,
sourcemap: false,
target: 'es2023',
lib: {
entry: 'src/renderer/app.tsx',
formats: ['cjs'],
fileName: () => 'renderer.js',
cssFileName: 'renderer',
},
rollupOptions: {
external: nodeExternals,
output: {
inlineDynamicImports: true,
assetFileNames: '[name][extname]',
// Electron renderer <script> with nodeIntegration has module/require
// but not a bare `exports` global; alias it so Rollup's CJS prelude works.
banner: 'var exports = module.exports;',
},
},
watch: watch ? {} : undefined,
},
}
}
async function compileVite(options = {}) {
if (!options.watch) {
await fs.promises.rm(path.join(root, 'dist'), { recursive: true, force: true })
}
await fs.promises.mkdir(path.join(root, 'dist/static'), { recursive: true })
await copyLib()
await build(mainConfig(options.watch))
await build(rendererConfig(options.watch))
}
module.exports = { compileVite }
if (require.main === module) compileVite()

3
tools/vite-watch.js Normal file
View File

@@ -0,0 +1,3 @@
const { compileVite } = require('./vite-build')
if (require.main === module) compileVite({ watch: true })

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"allowJs": true,
"allowSyntheticDefaultImports": true,