Files
windows95/tools/update-v86.js
Felix Rieseberg 45f5a136b2 Add SMB1 server and host folder share
Windows 95 can now mount a host folder as a network drive at \\HOST\HOST.
Read-only, ~1500 lines, zero deps. Defaults to ~/Downloads, configurable in
Settings.

Protocol: NEGOTIATE (LANMAN2.1), SESSION_SETUP, TREE_CONNECT, TRANSACTION/RAP
(NetShareEnum, NetServerGetInfo, NetWkstaGetInfo), TRANSACTION2/FIND_FIRST2,
SEARCH (8.3 with ~N suffix mapping), OPEN_ANDX, NT_CREATE_ANDX, READ_ANDX,
CLOSE, QUERY_INFORMATION, CHECK_DIRECTORY. NetBIOS Name Service on UDP 137
answers Node Status and Name Query so \\HOST resolves.

v86 hook: monkeypatches adapter.on_tcp_connection (old API), shadows
adapter.receive during a port-80 probe to steal a TCPConnection without
side effects, re-aims it at port 139. Data via .on_data (Closure
dead-code-eliminated .on/.emit). Also registers tcp-connection bus event
for newer v86 builds.

Security: read-only, path traversal blocked lexically and through symlinks
(realpath the deepest existing ancestor, re-append tail, confirm under root).
Share path validated in main-process IPC.

BIOS updated to SeaBIOS 1.16.2 (compatible with old v86). v86 itself stays
on the Feb 2025 prod build — newer builds hang at the splash screen on fresh
boot (bisect tooling included in tools/).

Also: tools/update-v86.js builds wasm+libv86+BIOS from a local v86 checkout
and refuses to install JS/wasm pairs more than 14 days apart (copy.sh ships
mismatched pairs). tools/parcel-build.js dynamic-import patch made tolerant
of post-d4c5fa86 builds.
2026-04-11 01:03:34 -07:00

152 lines
7.4 KiB
JavaScript

#!/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").
*
* Usage:
* node tools/update-v86.js [path/to/v86] # builds wasm from source
* node tools/update-v86.js --js-only # just download libv86.js
*
* 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.
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const { execSync } = 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;
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();
});
}
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);
});
}
async function main() {
const jsDest = path.join(LIB_DIR, 'libv86.js');
const wasmDest = path.join(LIB_DIR, 'build/v86.wasm');
// ─── 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)');
}
main().catch((e) => { console.error('✗', e.message); process.exit(1); });