From 935417cff1b28e9e8900fdd3f2c27caed5933753 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 22 Jan 2026 05:09:32 +0100 Subject: [PATCH] fix(pi-dosbox): include QBasic files in jsdos bundle The previous approach of writing files to Emscripten FS after DOSBox started didn't work because the C: drive mount was pointing elsewhere. Now we create a proper zip archive of QBasic files and include it in the jsdos bundle using the extract() API with a data URL. The bundle extracts to the C: drive root on startup. --- .../pi-dosbox/src/dosbox-instance.ts | 144 ++++++++++++------ 1 file changed, 100 insertions(+), 44 deletions(-) diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-instance.ts b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-instance.ts index 7a96dfd4..f91c68f2 100644 --- a/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-instance.ts +++ b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-instance.ts @@ -79,8 +79,7 @@ export class DosboxInstance { const bundle = await this.createBundle(emu); this.ci = await emu.dosboxDirect(bundle); - // Mount QBasic files to C: - await this.mountQBasic(); + // QBasic files are included in the bundle, no post-init mount needed const events = this.ci.events(); @@ -121,54 +120,120 @@ export class DosboxInstance { private async createBundle(emu: Emulators): Promise { const bundle = await emu.bundle(); + + // Add QBasic files to the bundle + // We'll create a zip and include it + await this.addQBasicToBundle(bundle); + + // Set up autoexec to show what's available bundle.autoexec( "@echo off", - "mount c c:", "c:", "cls", - "echo QuickBASIC 4.5 mounted at C:\\QB", + "echo QuickBASIC 4.5 is at C:\\QB", + "echo.", + "echo Type: CD QB", + "echo Then: QB.EXE", "echo.", - "dir /w", ); + return bundle.toUint8Array(true); } - private async mountQBasic(): Promise { - if (!this.ci) return; - - const fs = (this.ci as unknown as { transport: { module: { FS: EmscriptenFS } } }).transport.module.FS; - const rescan = (this.ci as unknown as { transport: { module: { _rescanFilesystem: () => void } } }).transport - .module._rescanFilesystem; - - // Create directory structure - try { - fs.mkdir("/c"); - } catch { - /* exists */ - } - try { - fs.mkdir("/c/QB"); - } catch { - /* exists */ - } - - // Read QBasic files from the extension directory + private async addQBasicToBundle(bundle: Awaited>): Promise { + // Read QBasic files and create a zip data URL const qbasicDir = join(__dirname, "..", "qbasic"); const { readdirSync, readFileSync } = await import("node:fs"); - const files = readdirSync(qbasicDir); + const files = readdirSync(qbasicDir).filter((f) => !f.startsWith(".")); + + // Create a simple zip structure + const zipParts: Buffer[] = []; + const centralDir: Buffer[] = []; + let offset = 0; + for (const file of files) { - if (file.startsWith(".")) continue; - try { - const data = readFileSync(join(qbasicDir, file)); - fs.writeFile(`/c/QB/${file.toUpperCase()}`, data); - } catch (e) { - console.error(`Failed to mount ${file}:`, e); - } + const data = readFileSync(join(qbasicDir, file)); + const fileName = `QB/${file.toUpperCase()}`; + const fileNameBuf = Buffer.from(fileName, "utf-8"); + + // Local file header + const localHeader = Buffer.alloc(30 + fileNameBuf.length); + localHeader.writeUInt32LE(0x04034b50, 0); // signature + localHeader.writeUInt16LE(20, 4); // version needed + localHeader.writeUInt16LE(0, 6); // flags + localHeader.writeUInt16LE(0, 8); // compression (none) + localHeader.writeUInt16LE(0, 10); // mod time + localHeader.writeUInt16LE(0, 12); // mod date + localHeader.writeUInt32LE(this.crc32Buf(data), 14); // crc32 + localHeader.writeUInt32LE(data.length, 18); // compressed size + localHeader.writeUInt32LE(data.length, 22); // uncompressed size + localHeader.writeUInt16LE(fileNameBuf.length, 26); // filename length + localHeader.writeUInt16LE(0, 28); // extra field length + fileNameBuf.copy(localHeader, 30); + + zipParts.push(localHeader); + zipParts.push(data); + + // Central directory entry + const cdEntry = Buffer.alloc(46 + fileNameBuf.length); + cdEntry.writeUInt32LE(0x02014b50, 0); // signature + cdEntry.writeUInt16LE(20, 4); // version made by + cdEntry.writeUInt16LE(20, 6); // version needed + cdEntry.writeUInt16LE(0, 8); // flags + cdEntry.writeUInt16LE(0, 10); // compression + cdEntry.writeUInt16LE(0, 12); // mod time + cdEntry.writeUInt16LE(0, 14); // mod date + cdEntry.writeUInt32LE(this.crc32Buf(data), 16); // crc32 + cdEntry.writeUInt32LE(data.length, 20); // compressed size + cdEntry.writeUInt32LE(data.length, 24); // uncompressed size + cdEntry.writeUInt16LE(fileNameBuf.length, 28); // filename length + cdEntry.writeUInt16LE(0, 30); // extra field length + cdEntry.writeUInt16LE(0, 32); // comment length + cdEntry.writeUInt16LE(0, 34); // disk number start + cdEntry.writeUInt16LE(0, 36); // internal attrs + cdEntry.writeUInt32LE(0, 38); // external attrs + cdEntry.writeUInt32LE(offset, 42); // relative offset + fileNameBuf.copy(cdEntry, 46); + + centralDir.push(cdEntry); + offset += localHeader.length + data.length; } - // Rescan so DOS sees the new files - rescan(); + const cdOffset = offset; + const cdSize = centralDir.reduce((sum, buf) => sum + buf.length, 0); + + // End of central directory + const eocd = Buffer.alloc(22); + eocd.writeUInt32LE(0x06054b50, 0); // signature + eocd.writeUInt16LE(0, 4); // disk number + eocd.writeUInt16LE(0, 6); // disk with CD + eocd.writeUInt16LE(files.length, 8); // entries on disk + eocd.writeUInt16LE(files.length, 10); // total entries + eocd.writeUInt32LE(cdSize, 12); // CD size + eocd.writeUInt32LE(cdOffset, 16); // CD offset + eocd.writeUInt16LE(0, 20); // comment length + + const zipBuffer = Buffer.concat([...zipParts, ...centralDir, eocd]); + const base64 = zipBuffer.toString("base64"); + const dataUrl = `data:application/zip;base64,${base64}`; + + // Extract to root of C: drive + bundle.extract(dataUrl, "/", "zip"); + } + + private crc32Buf(buffer: Buffer): number { + let crc = 0xffffffff; + for (const byte of buffer) { + crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; + } + + // No longer needed - files are in bundle + // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Keeping for reference + private async mountQBasicPostInit(): Promise { + // This approach didn't work - keeping for reference } private expandRgbToRgba(rgb: Uint8Array): Uint8Array { @@ -417,12 +482,3 @@ function createCrcTable(): Uint32Array { } return table; } - -// Emscripten FS type -interface EmscriptenFS { - mkdir(path: string): void; - writeFile(path: string, data: Uint8Array | Buffer): void; - readFile(path: string): Uint8Array; - readdir(path: string): string[]; - unlink(path: string): void; -}