${getParentFolderLinkHtml(parentPath)} | ${getDesktopLinkHtml()} | ${getDownloadsLinkHtml()}
--
windows95 failed to find the file or directory on your host computer: ${requestedPath}
Options:
- - - - `; -} diff --git a/src/main/settings.ts b/src/main/settings.ts index e35ce25..23277c5 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -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(); diff --git a/src/renderer/smb/README.md b/src/renderer/smb/README.md index 855e4ab..bb5ea27 100644 --- a/src/renderer/smb/README.md +++ b/src/renderer/smb/README.md @@ -97,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` — 48 protocol tests, full round-trips with real file I/O. +`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` diff --git a/src/renderer/smb/server.ts b/src/renderer/smb/server.ts index bd98d33..393fa07 100644 --- a/src/renderer/smb/server.ts +++ b/src/renderer/smb/server.ts @@ -44,8 +44,9 @@ interface OpenFile { } 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 }; } @@ -54,9 +55,26 @@ 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. @@ -140,7 +158,7 @@ export class SmbSession { const now = new Date(); const stat = (size: number) => ({ isDirectory: () => false, size, mtime: now }); return Array.from(this.virtuals, ([name, bytes]) => - ({ name, sfn: name, stat: stat(bytes.length) })); + ({ name, sfn: name, attr: ATTR_ARCHIVE, stat: stat(bytes.length) })); } /** @@ -155,6 +173,7 @@ export class SmbSession { this.sfnMaps.set(hostDir, sfnMap); const entries: DirEntry[] = []; + const aliases: [string, string][] = []; for (const [sfn, real] of sfnMap) { try { // The long name we send is single-byte OEM. Anything outside that @@ -162,11 +181,13 @@ export class SmbSession { // 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 = oemSafe(real); - if (name !== real) sfnMap.set(name.toUpperCase(), real); - entries.push({ name, sfn, stat: fs.statSync(path.join(hostDir, real)) }); + 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 */ } } + for (const [k, v] of aliases) sfnMap.set(k, v); return entries; } @@ -446,9 +467,8 @@ export class SmbSession { 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 @@ -508,13 +528,13 @@ export class SmbSession { const fid = this.nextFid++; this.fids.set(fid, { hostPath: `I've installed a few apps and games for you to try out. Check out the Games folder on the desktop!
-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 your host's Download folder.
+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 the network share — open Network Neighborhood on the desktop (or drive Z: after running \\HOST\TOOLS\_MAPZ.BAT).