mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-14 18:31:59 +00:00
Compare commits
39 Commits
claude/sha
...
v5.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c82c5d09 | ||
|
|
60ee631575 | ||
|
|
dddaca9120 | ||
|
|
d383958f2b | ||
|
|
a1637b1de1 | ||
|
|
fff371073d | ||
|
|
8153e91706 | ||
|
|
fb701041c2 | ||
|
|
e1c30f701c | ||
|
|
bbcc4f32cd | ||
|
|
9c99b6cb65 | ||
|
|
2e01152d82 | ||
|
|
63f02842bc | ||
|
|
c847467de6 | ||
|
|
fa0e4c691e | ||
|
|
6c964f99e9 | ||
|
|
1f116d607b | ||
|
|
d38355ff16 | ||
|
|
b74e6c7b0a | ||
|
|
3c63139fae | ||
|
|
bc76e9c79a | ||
|
|
6e73df11ae | ||
|
|
5da7f94c5a | ||
|
|
766497bd5d | ||
|
|
85c44513cb | ||
|
|
ccd2b28169 | ||
|
|
27b9e0eb7a | ||
|
|
1dbb853fe6 | ||
|
|
9b217731f5 | ||
|
|
148f8e4874 | ||
|
|
43c025929b | ||
|
|
3b62e1c9b5 | ||
|
|
e9ddfab65d | ||
|
|
89c0a8575d | ||
|
|
2ef06eb412 | ||
|
|
3eab26fed1 | ||
|
|
1bef3cce62 | ||
|
|
17e0182ecb | ||
|
|
20f7f8c70e |
122
.claude/skills/probe-win95/SKILL.md
Normal file
122
.claude/skills/probe-win95/SKILL.md
Normal 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. |
|
||||
151
.claude/skills/update-v86/SKILL.md
Normal file
151
.claude/skills/update-v86/SKILL.md
Normal 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
|
||||
6–9 so `W95TOOLS.EXE` (guest-tools/agent) can sync `CF_TEXT` with
|
||||
the host. Consumes `mouse-absolute` and `vmware-clipboard-host` bus
|
||||
events; emits `vmware-absolute-mouse` and `vmware-clipboard-guest`.
|
||||
- **`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 3B0–3DF 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`.
|
||||
79
.github/workflows/build.yml
vendored
79
.github/workflows/build.yml
vendored
@@ -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
5
.gitignore
vendored
@@ -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/
|
||||
|
||||
37
README.md
37
README.md
@@ -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
45
docs/smb-share.md
Normal 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
25
docs/v86-patches.md
Normal 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 6–9 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`.
|
||||
@@ -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
45
guest-tools/README.md
Normal 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 6–9; 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).
|
||||
12
guest-tools/agent/Dockerfile
Normal file
12
guest-tools/agent/Dockerfile
Normal 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
|
||||
30
guest-tools/agent/Makefile
Normal file
30
guest-tools/agent/Makefile
Normal 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
|
||||
222
guest-tools/agent/w95tools.c
Normal file
222
guest-tools/agent/w95tools.c
Normal 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;
|
||||
}
|
||||
6
guest-tools/mouse-driver/LICENSE.TXT
Normal file
6
guest-tools/mouse-driver/LICENSE.TXT
Normal 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
|
||||
8
guest-tools/mouse-driver/OEMSETUP.INF
Normal file
8
guest-tools/mouse-driver/OEMSETUP.INF
Normal 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
|
||||
BIN
guest-tools/mouse-driver/VBMOUSE.DRV
Normal file
BIN
guest-tools/mouse-driver/VBMOUSE.DRV
Normal file
Binary file not shown.
54
guest-tools/mouse-driver/VBMOUSE.EN
Normal file
54
guest-tools/mouse-driver/VBMOUSE.EN
Normal 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
14296
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
font-family: @win-font;
|
||||
font-family: var(--win-font);
|
||||
}
|
||||
|
||||
input[type="text"]:read-only {
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Welcome to Windows" splash — modelled on the real first-boot dialog.
|
||||
/* "Welcome to Windows" splash — modelled 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;
|
||||
@@ -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%);
|
||||
@@ -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">`;
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import "../css/vendor/98.css";
|
||||
import "../css/root.css";
|
||||
|
||||
export interface Win95Window extends Window {
|
||||
emulator: any;
|
||||
win95: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
62
src/renderer/clipboard.ts
Normal 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 6–9) implemented in v86's vmware.js. Inside the guest, W95TOOLS.EXE
|
||||
// (guest-tools/agent) polls the backdoor and bridges it to CF_TEXT via the
|
||||
// Win32 clipboard-viewer chain. Out here we poll Electron's clipboard — there
|
||||
// is no change event — and translate between host UTF-8/LF and guest
|
||||
// Windows-1252/CRLF.
|
||||
|
||||
import { clipboard } from "electron";
|
||||
|
||||
const CP1252 = new TextDecoder("windows-1252");
|
||||
// v86's vmware.js and the guest agent both clamp to 64 KB; clamp here too so
|
||||
// a huge host clipboard never even gets allocated/encoded.
|
||||
const CLIP_MAX = 0x10000;
|
||||
|
||||
function fromGuest(bytes: Uint8Array): string {
|
||||
return CP1252.decode(bytes).replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
function toGuest(text: string): Uint8Array {
|
||||
const s = text.slice(0, CLIP_MAX).replace(/\r\n|\n/g, "\r\n");
|
||||
const n = Math.min(s.length, CLIP_MAX);
|
||||
const out = new Uint8Array(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const c = s.charCodeAt(i);
|
||||
out[i] = c < 256 ? c : 0x3f;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function setupClipboardSync(emulator: any): () => void {
|
||||
// Track the last value seen on each side so a value we just wrote doesn't
|
||||
// bounce back as a "change" from the other side.
|
||||
let lastHost = clipboard.readText();
|
||||
let lastGuest = "";
|
||||
|
||||
emulator.add_listener("vmware-clipboard-guest", (bytes: Uint8Array) => {
|
||||
const text = fromGuest(bytes);
|
||||
if (text === lastHost || text === lastGuest) return;
|
||||
lastGuest = text;
|
||||
lastHost = text;
|
||||
clipboard.writeText(text);
|
||||
console.log("[clip] guest → host", text.length, "chars");
|
||||
});
|
||||
|
||||
const poll = () => {
|
||||
const text = clipboard.readText();
|
||||
if (text === lastHost) return;
|
||||
lastHost = text;
|
||||
if (text === lastGuest) return;
|
||||
emulator.bus.send("vmware-clipboard-host", toGuest(text));
|
||||
console.log("[clip] host → guest", text.length, "chars");
|
||||
};
|
||||
|
||||
const id = window.setInterval(poll, 500);
|
||||
window.addEventListener("focus", poll);
|
||||
return () => {
|
||||
window.clearInterval(id);
|
||||
window.removeEventListener("focus", poll);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
29
src/renderer/info-bar-settings.ts
Normal file
29
src/renderer/info-bar-settings.ts
Normal 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 {}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
81
src/renderer/net/dns-shim.ts
Normal file
81
src/renderer/net/dns-shim.ts
Normal 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;
|
||||
}
|
||||
153
src/renderer/net/tcp-relay.ts
Normal file
153
src/renderer/net/tcp-relay.ts
Normal 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");
|
||||
}
|
||||
118
src/renderer/net/tcp-trace.ts
Normal file
118
src/renderer/net/tcp-trace.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
1
src/renderer/styles.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module "*.css";
|
||||
50
src/renderer/sync-file-buffer.ts
Normal file
50
src/renderer/sync-file-buffer.ts
Normal 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) {}
|
||||
}
|
||||
132
src/renderer/utils/fat32-extract.ts
Normal file
132
src/renderer/utils/fat32-extract.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
88
src/renderer/utils/recover-legacy-disk.ts
Normal file
88
src/renderer/utils/recover-legacy-disk.ts
Normal 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 };
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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> — that's the shared folder from your host machine.</p>
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
35
tools/download-disk.sh
Normal file → Executable 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/
|
||||
|
||||
@@ -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
9
tools/pack-disk.sh
Executable 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"
|
||||
@@ -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()
|
||||
@@ -1,11 +0,0 @@
|
||||
const { compileParcel } = require('./parcel-build')
|
||||
|
||||
async function watchParcel () {
|
||||
return compileParcel({ watch: true })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
watchParcel
|
||||
}
|
||||
|
||||
if (require.main === module) watchParcel()
|
||||
@@ -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
65
tools/probe-tcp.sh
Executable 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 ]
|
||||
@@ -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
113
tools/vite-build.js
Normal 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
3
tools/vite-watch.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const { compileVite } = require('./vite-build')
|
||||
|
||||
if (require.main === module) compileVite({ watch: true })
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
Reference in New Issue
Block a user