mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-09 00:24:09 +00:00
Add SMB READ_RAW support for faster bulk transfers (#366)
Win95's redirector was falling back to ~2.8KB core READs (no raw mode advertised), so a 200KB copy took ~360 round-trips through the emulated NIC/TCP stack (~10s). With READ_RAW it pulls up to 64KB per round-trip. - server.ts: readRaw() handler (raw bytes only, 0-byte frame on error); negotiate now sets MaxRawSize=65535 + CAP_RAW_MODE (NT) and RawMode bit 0 (LM); per-packet hex capture gated on WIN95_SMB_CAPTURE - smb.ts: CMD_READ_RAW constant - test-standalone.ts: READ_RAW happy-path + bad-fid tests (57 pass)
This commit is contained in:
@@ -8,7 +8,7 @@ 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, CMD_READ_ANDX, CMD_SEEK, 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_FS_INFO, TRANS2_QUERY_PATH_INFO,
|
||||
ERRDOS, ERRSRV, ERR_BADFILE, ERR_BADPATH, ERR_BADFID, ERR_NOFILES, ERR_BADFUNC,
|
||||
@@ -112,7 +112,7 @@ export class SmbSession {
|
||||
private readonly realRoot: string;
|
||||
private readonly toolsRoot?: string;
|
||||
public readonly shareName: string;
|
||||
public capture = true;
|
||||
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 —
|
||||
@@ -241,6 +241,10 @@ export class SmbSession {
|
||||
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);
|
||||
@@ -304,9 +308,9 @@ export class SmbSession {
|
||||
.u16(1) // MaxMpxCount
|
||||
.u16(1) // MaxNumberVcs
|
||||
.u32(16384) // MaxBufferSize
|
||||
.u32(0) // MaxRawSize (no raw)
|
||||
.u32(65535) // MaxRawSize
|
||||
.u32(0) // SessionKey
|
||||
.u32(0) // Capabilities
|
||||
.u32(0x00000001) // Capabilities: CAP_RAW_MODE only
|
||||
.u64(0) // SystemTime (FILETIME — Win95 ignores 0)
|
||||
.u16(0) // ServerTimeZone
|
||||
.u8(0) // ChallengeLength = 0
|
||||
@@ -324,7 +328,7 @@ export class SmbSession {
|
||||
.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
|
||||
@@ -670,16 +674,28 @@ export class SmbSession {
|
||||
return buildSmb(req, CMD_READ, 0, words, bytes);
|
||||
}
|
||||
|
||||
private readBytes(fid: number, offset: number, count: number): Uint8Array | null {
|
||||
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, 16384, Math.max(0, file.size - offset));
|
||||
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
|
||||
|
||||
@@ -17,6 +17,7 @@ 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;
|
||||
@@ -145,6 +146,7 @@ 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",
|
||||
|
||||
@@ -411,6 +411,17 @@ console.log("\n[7] Error handling");
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user