typescript-virtual-container 1.5.5 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -35
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/index.d.ts +5 -1
- package/dist/SSHMimic/index.js +27 -3
- package/dist/SSHMimic/scp.d.ts +34 -0
- package/dist/SSHMimic/scp.js +285 -0
- package/dist/SSHMimic/sftp.d.ts +53 -3
- package/dist/SSHMimic/sftp.js +9 -3
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
- package/dist/VirtualFileSystem/binaryPack.js +37 -1
- package/dist/VirtualFileSystem/index.d.ts +7 -0
- package/dist/VirtualFileSystem/index.js +67 -27
- package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
- package/dist/VirtualFileSystem/path.d.ts +5 -0
- package/dist/VirtualFileSystem/path.js +24 -11
- package/dist/VirtualPackageManager/index.d.ts +4 -2
- package/dist/VirtualPackageManager/index.js +24 -4
- package/dist/VirtualShell/index.d.ts +4 -0
- package/dist/VirtualShell/index.js +1 -7
- package/dist/VirtualShell/shell.js +40 -10
- package/dist/VirtualShell/shellParser.js +1 -22
- package/dist/commands/awk.d.ts +6 -11
- package/dist/commands/awk.js +462 -109
- package/dist/commands/bzip2.d.ts +11 -0
- package/dist/commands/bzip2.js +91 -0
- package/dist/commands/exit.js +1 -1
- package/dist/commands/find.d.ts +2 -2
- package/dist/commands/find.js +209 -37
- package/dist/commands/helpers.d.ts +0 -20
- package/dist/commands/helpers.js +0 -97
- package/dist/commands/lsof.d.ts +6 -0
- package/dist/commands/lsof.js +30 -0
- package/dist/commands/perl.d.ts +6 -0
- package/dist/commands/perl.js +76 -0
- package/dist/commands/python.js +5 -2
- package/dist/commands/registry.js +19 -1
- package/dist/commands/runtime.js +65 -87
- package/dist/commands/sed.d.ts +2 -2
- package/dist/commands/sed.js +216 -34
- package/dist/commands/sh.js +42 -0
- package/dist/commands/strace.d.ts +6 -0
- package/dist/commands/strace.js +26 -0
- package/dist/commands/tar.d.ts +2 -1
- package/dist/commands/tar.js +138 -52
- package/dist/commands/test.js +2 -2
- package/dist/commands/zip.d.ts +11 -0
- package/dist/commands/zip.js +232 -0
- package/dist/modules/linuxRootfs.js +1 -4
- package/dist/modules/neofetch.js +2 -2
- package/dist/types/commands.d.ts +4 -0
- package/dist/utils/argv.d.ts +6 -0
- package/dist/utils/argv.js +32 -0
- package/dist/utils/expand.d.ts +5 -2
- package/dist/utils/expand.js +112 -45
- package/dist/utils/glob.d.ts +6 -0
- package/dist/utils/glob.js +34 -0
- package/dist/utils/tokenize.js +13 -13
- package/package.json +9 -7
- package/dist/self-standalone.d.ts +0 -1
- package/dist/self-standalone.js +0 -444
- package/dist/standalone-wo-sftp.d.ts +0 -1
- package/dist/standalone-wo-sftp.js +0 -30
- package/dist/standalone.d.ts +0 -1
- package/dist/standalone.js +0 -61
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCP (Secure Copy Protocol) server-side implementation over SSH exec.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* scp -t [-r] <dest> — sink mode (client → server, uploading)
|
|
6
|
+
* scp -f [-r] <src> — source mode (server → client, downloading)
|
|
7
|
+
*
|
|
8
|
+
* The SCP wire protocol is a line-oriented binary protocol sent over the
|
|
9
|
+
* SSH exec channel. Control messages are ASCII lines terminated with \n.
|
|
10
|
+
* File data immediately follows the ready acknowledgement (\0).
|
|
11
|
+
*
|
|
12
|
+
* Sink control messages (client → server):
|
|
13
|
+
* C<mode> <size> <name>\n — regular file
|
|
14
|
+
* D<mode> 0 <name>\n — enter directory
|
|
15
|
+
* E\n — leave directory
|
|
16
|
+
* T<mtime_s> 0 <atime_s> 0\n — timestamps (ignored)
|
|
17
|
+
*
|
|
18
|
+
* Acknowledgement byte: \0 = ok, \1 = warning, \2 = error.
|
|
19
|
+
*/
|
|
20
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
21
|
+
interface ScpStream {
|
|
22
|
+
write(data: Buffer | string): boolean;
|
|
23
|
+
on(event: "data", listener: (chunk: Buffer) => void): this;
|
|
24
|
+
on(event: "end" | "close", listener: () => void): this;
|
|
25
|
+
stderr: {
|
|
26
|
+
write(data: string | Buffer): void;
|
|
27
|
+
};
|
|
28
|
+
exit(code: number): void;
|
|
29
|
+
end(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function runScpSink(stream: ScpStream, destArg: string, authUser: string, shell: VirtualShell, recursive: boolean): void;
|
|
32
|
+
export declare function runScpSource(stream: ScpStream, srcArg: string, _authUser: string, shell: VirtualShell, recursive: boolean): void;
|
|
33
|
+
export declare function handleScp(stream: ScpStream, rawArgs: string[], authUser: string, shell: VirtualShell): void;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCP (Secure Copy Protocol) server-side implementation over SSH exec.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* scp -t [-r] <dest> — sink mode (client → server, uploading)
|
|
6
|
+
* scp -f [-r] <src> — source mode (server → client, downloading)
|
|
7
|
+
*
|
|
8
|
+
* The SCP wire protocol is a line-oriented binary protocol sent over the
|
|
9
|
+
* SSH exec channel. Control messages are ASCII lines terminated with \n.
|
|
10
|
+
* File data immediately follows the ready acknowledgement (\0).
|
|
11
|
+
*
|
|
12
|
+
* Sink control messages (client → server):
|
|
13
|
+
* C<mode> <size> <name>\n — regular file
|
|
14
|
+
* D<mode> 0 <name>\n — enter directory
|
|
15
|
+
* E\n — leave directory
|
|
16
|
+
* T<mtime_s> 0 <atime_s> 0\n — timestamps (ignored)
|
|
17
|
+
*
|
|
18
|
+
* Acknowledgement byte: \0 = ok, \1 = warning, \2 = error.
|
|
19
|
+
*/
|
|
20
|
+
import { basename } from "node:path";
|
|
21
|
+
import { resolvePath } from "../modules/shellRuntime";
|
|
22
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
23
|
+
const ACK = Buffer.from([0x00]);
|
|
24
|
+
const ERR = (msg) => Buffer.from(`\x02${msg}\n`);
|
|
25
|
+
function scpError(stream, msg, code = 1) {
|
|
26
|
+
stream.stderr.write(`scp: ${msg}\n`);
|
|
27
|
+
stream.write(ERR(`scp: ${msg}`));
|
|
28
|
+
stream.exit(code);
|
|
29
|
+
stream.end();
|
|
30
|
+
}
|
|
31
|
+
function parseArgs(args) {
|
|
32
|
+
const flags = args.filter((a) => a.startsWith("-")).join("");
|
|
33
|
+
return {
|
|
34
|
+
sink: flags.includes("t"),
|
|
35
|
+
source: flags.includes("f"),
|
|
36
|
+
recursive: flags.includes("r"),
|
|
37
|
+
preserve: flags.includes("p"),
|
|
38
|
+
target: args.find((a) => !a.startsWith("-")),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ── Sink mode (upload: client → server) ──────────────────────────────────────
|
|
42
|
+
export function runScpSink(stream, destArg, authUser, shell, recursive) {
|
|
43
|
+
// Buffered reader that handles arbitrary chunk boundaries
|
|
44
|
+
let buf = Buffer.alloc(0);
|
|
45
|
+
const dirStack = [resolvePath("/", destArg)];
|
|
46
|
+
let state = "cmd";
|
|
47
|
+
let pendingFile = null;
|
|
48
|
+
let bytesRead = 0;
|
|
49
|
+
const fileChunks = [];
|
|
50
|
+
let finished = false;
|
|
51
|
+
function currentDir() {
|
|
52
|
+
return dirStack.at(-1) ?? "/";
|
|
53
|
+
}
|
|
54
|
+
function send(data) {
|
|
55
|
+
stream.write(typeof data === "string" ? Buffer.from(data) : data);
|
|
56
|
+
}
|
|
57
|
+
function processCmd(line) {
|
|
58
|
+
const type = line[0];
|
|
59
|
+
// Timestamps — ignored, just ack
|
|
60
|
+
if (type === "T") {
|
|
61
|
+
send(ACK);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// End directory
|
|
65
|
+
if (type === "E") {
|
|
66
|
+
dirStack.pop();
|
|
67
|
+
send(ACK);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// File or directory header: C<mode> <size> <name> / D<mode> 0 <name>
|
|
71
|
+
if (type === "C" || type === "D") {
|
|
72
|
+
const parts = line.slice(1).split(" ");
|
|
73
|
+
const name = parts.slice(2).join(" "); // name may contain spaces
|
|
74
|
+
const size = Number(parts[1] ?? "0");
|
|
75
|
+
if (!name || name === "." || name === "..") {
|
|
76
|
+
send(ERR("invalid filename"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (type === "D") {
|
|
80
|
+
if (!recursive) {
|
|
81
|
+
send(ERR("not a regular file"));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const dir = `${currentDir()}/${name}`;
|
|
85
|
+
if (!shell.vfs.exists(dir)) {
|
|
86
|
+
try {
|
|
87
|
+
shell.vfs.mkdir(dir, 0o755);
|
|
88
|
+
}
|
|
89
|
+
catch { /* already exists */ }
|
|
90
|
+
}
|
|
91
|
+
dirStack.push(dir);
|
|
92
|
+
send(ACK);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// type === "C"
|
|
96
|
+
const destPath = `${currentDir()}/${name}`;
|
|
97
|
+
pendingFile = { name, size, dest: destPath };
|
|
98
|
+
bytesRead = 0;
|
|
99
|
+
fileChunks.length = 0;
|
|
100
|
+
state = "data";
|
|
101
|
+
send(ACK);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
send(ERR(`unknown control message: ${line[0]}`));
|
|
105
|
+
}
|
|
106
|
+
function processBuffer() {
|
|
107
|
+
while (buf.length > 0) {
|
|
108
|
+
if (state === "cmd") {
|
|
109
|
+
const nl = buf.indexOf(0x0a); // \n
|
|
110
|
+
if (nl === -1)
|
|
111
|
+
break; // wait for more
|
|
112
|
+
const line = buf.subarray(0, nl).toString("utf8").replace(/\r$/, "");
|
|
113
|
+
buf = buf.subarray(nl + 1);
|
|
114
|
+
processCmd(line);
|
|
115
|
+
}
|
|
116
|
+
else if (state === "data" && pendingFile) {
|
|
117
|
+
const remaining = pendingFile.size - bytesRead;
|
|
118
|
+
if (remaining > 0) {
|
|
119
|
+
const take = Math.min(buf.length, remaining);
|
|
120
|
+
fileChunks.push(buf.subarray(0, take));
|
|
121
|
+
buf = buf.subarray(take);
|
|
122
|
+
bytesRead += take;
|
|
123
|
+
}
|
|
124
|
+
if (bytesRead >= pendingFile.size) {
|
|
125
|
+
// Next byte must be \0 (end-of-file marker from client)
|
|
126
|
+
if (buf.length === 0)
|
|
127
|
+
break;
|
|
128
|
+
const marker = buf[0];
|
|
129
|
+
buf = buf.subarray(1);
|
|
130
|
+
if (marker !== 0x00) {
|
|
131
|
+
scpError(stream, "protocol error: expected \\0 after file data");
|
|
132
|
+
finished = true;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Write file to VFS
|
|
136
|
+
const content = Buffer.concat(fileChunks);
|
|
137
|
+
try {
|
|
138
|
+
shell.writeFileAsUser(authUser, pendingFile.dest, content);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
send(ERR(`cannot write ${pendingFile.dest}: ${String(e)}`));
|
|
142
|
+
finished = true;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
pendingFile = null;
|
|
146
|
+
state = "cmd";
|
|
147
|
+
send(ACK);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Send initial ready signal
|
|
156
|
+
send(ACK);
|
|
157
|
+
stream.on("data", (chunk) => {
|
|
158
|
+
if (finished)
|
|
159
|
+
return;
|
|
160
|
+
buf = Buffer.concat([buf, chunk]);
|
|
161
|
+
processBuffer();
|
|
162
|
+
});
|
|
163
|
+
stream.on("end", () => {
|
|
164
|
+
stream.exit(0);
|
|
165
|
+
stream.end();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// ── Source mode (download: server → client) ───────────────────────────────────
|
|
169
|
+
export function runScpSource(stream, srcArg, _authUser, shell, recursive) {
|
|
170
|
+
const srcPath = resolvePath("/", srcArg);
|
|
171
|
+
if (!shell.vfs.exists(srcPath)) {
|
|
172
|
+
scpError(stream, `${srcArg}: No such file or directory`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const entries = [];
|
|
176
|
+
function collect(p, name) {
|
|
177
|
+
const st = shell.vfs.stat(p);
|
|
178
|
+
if (st.type === "directory") {
|
|
179
|
+
if (!recursive) {
|
|
180
|
+
// skip directories silently like real scp
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
entries.push({ kind: "dir-open", name });
|
|
184
|
+
for (const child of shell.vfs.list(p)) {
|
|
185
|
+
collect(`${p}/${child}`, child);
|
|
186
|
+
}
|
|
187
|
+
entries.push({ kind: "dir-close" });
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
entries.push({ kind: "file", path: p, name });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
collect(srcPath, basename(srcPath));
|
|
194
|
+
if (entries.length === 0) {
|
|
195
|
+
stream.exit(0);
|
|
196
|
+
stream.end();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// State machine driven by \0 acks from client
|
|
200
|
+
let idx = 0;
|
|
201
|
+
let sendingData = false;
|
|
202
|
+
let pendingData = null;
|
|
203
|
+
function sendNext() {
|
|
204
|
+
if (idx >= entries.length) {
|
|
205
|
+
stream.exit(0);
|
|
206
|
+
stream.end();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const entry = entries[idx];
|
|
210
|
+
idx++;
|
|
211
|
+
if (entry.kind === "dir-open") {
|
|
212
|
+
stream.write(`D0755 0 ${entry.name}\n`);
|
|
213
|
+
// wait for ack, then recurse
|
|
214
|
+
}
|
|
215
|
+
else if (entry.kind === "dir-close") {
|
|
216
|
+
stream.write("E\n");
|
|
217
|
+
// wait for ack
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const content = shell.vfs.readFileRaw(entry.path);
|
|
221
|
+
stream.write(`C0644 ${content.length} ${entry.name}\n`);
|
|
222
|
+
pendingData = content;
|
|
223
|
+
sendingData = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let buf = Buffer.alloc(0);
|
|
227
|
+
let waitingForDataAck = false;
|
|
228
|
+
function processAcks() {
|
|
229
|
+
while (buf.length > 0) {
|
|
230
|
+
const byte = buf[0];
|
|
231
|
+
buf = buf.subarray(1);
|
|
232
|
+
if (byte === 0x01 || byte === 0x02) {
|
|
233
|
+
// Error from client
|
|
234
|
+
stream.exit(1);
|
|
235
|
+
stream.end();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// byte === 0x00: ack
|
|
239
|
+
if (waitingForDataAck) {
|
|
240
|
+
waitingForDataAck = false;
|
|
241
|
+
sendNext();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (sendingData && pendingData !== null) {
|
|
245
|
+
// Client acked the header — send data then \0
|
|
246
|
+
stream.write(pendingData);
|
|
247
|
+
stream.write(ACK);
|
|
248
|
+
pendingData = null;
|
|
249
|
+
sendingData = false;
|
|
250
|
+
waitingForDataAck = true;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
sendNext();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
stream.on("data", (chunk) => {
|
|
258
|
+
buf = Buffer.concat([buf, chunk]);
|
|
259
|
+
processAcks();
|
|
260
|
+
});
|
|
261
|
+
stream.on("end", () => {
|
|
262
|
+
stream.exit(0);
|
|
263
|
+
stream.end();
|
|
264
|
+
});
|
|
265
|
+
// Wait for initial \0 from client before sending anything
|
|
266
|
+
// (processAcks will call sendNext on first ack)
|
|
267
|
+
}
|
|
268
|
+
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
269
|
+
export function handleScp(stream, rawArgs, authUser, shell) {
|
|
270
|
+
const { sink, source, recursive, target } = parseArgs(rawArgs);
|
|
271
|
+
if (!sink && !source) {
|
|
272
|
+
scpError(stream, "missing -t or -f flag");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!target) {
|
|
276
|
+
scpError(stream, "missing target path");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (sink) {
|
|
280
|
+
runScpSink(stream, target, authUser, shell, recursive);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
runScpSource(stream, target, authUser, shell, recursive);
|
|
284
|
+
}
|
|
285
|
+
}
|
package/dist/SSHMimic/sftp.d.ts
CHANGED
|
@@ -4,16 +4,60 @@ import { Server as SshServer } from "ssh2";
|
|
|
4
4
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
5
5
|
import { VirtualShell } from "../VirtualShell";
|
|
6
6
|
import type { VirtualUserManager } from "../VirtualUserManager";
|
|
7
|
+
/** @internal */
|
|
8
|
+
interface SftpAttributes {
|
|
9
|
+
mode: number;
|
|
10
|
+
uid: number;
|
|
11
|
+
gid: number;
|
|
12
|
+
size: number;
|
|
13
|
+
atime: number | Date;
|
|
14
|
+
mtime: number | Date;
|
|
15
|
+
}
|
|
16
|
+
/** @internal */
|
|
17
|
+
interface SftpServerStream {
|
|
18
|
+
on(event: "OPEN", listener: (reqid: number, filename: string, flags: number) => void): this;
|
|
19
|
+
on(event: "READ", listener: (reqid: number, handle: Buffer, offset: number, length: number) => void): this;
|
|
20
|
+
on(event: "WRITE", listener: (reqid: number, handle: Buffer, offset: number, data: Buffer) => void): this;
|
|
21
|
+
on(event: "FSTAT", listener: (reqid: number, handle: Buffer) => void): this;
|
|
22
|
+
on(event: "CLOSE", listener: (reqid: number, handle: Buffer) => void): this;
|
|
23
|
+
on(event: "OPENDIR", listener: (reqid: number, path: string) => void): this;
|
|
24
|
+
on(event: "READDIR", listener: (reqid: number, handle: Buffer) => void): this;
|
|
25
|
+
on(event: "STAT", listener: (reqid: number, path: string) => void): this;
|
|
26
|
+
on(event: "LSTAT", listener: (reqid: number, path: string) => void): this;
|
|
27
|
+
on(event: "FSETSTAT", listener: (reqid: number, handle: Buffer, attrs: Partial<SftpAttributes>) => void): this;
|
|
28
|
+
on(event: "SETSTAT", listener: (reqid: number, path: string, attrs: Partial<SftpAttributes>) => void): this;
|
|
29
|
+
on(event: "REALPATH", listener: (reqid: number, path: string) => void): this;
|
|
30
|
+
on(event: "MKDIR", listener: (reqid: number, path: string) => void): this;
|
|
31
|
+
on(event: "RMDIR", listener: (reqid: number, path: string) => void): this;
|
|
32
|
+
on(event: "REMOVE", listener: (reqid: number, path: string) => void): this;
|
|
33
|
+
on(event: "RENAME", listener: (reqid: number, oldPath: string, newPath: string) => void): this;
|
|
34
|
+
on(event: "READLINK", listener: (reqid: number) => void): this;
|
|
35
|
+
on(event: "SYMLINK", listener: (reqid: number) => void): this;
|
|
36
|
+
on(event: "END", listener: () => void): this;
|
|
37
|
+
on(event: "end", listener: () => void): this;
|
|
38
|
+
on(event: "error", listener: (error: Error) => void): this;
|
|
39
|
+
on(event: "close", listener: () => void): this;
|
|
40
|
+
status(reqid: number, code: number): void;
|
|
41
|
+
attrs(reqid: number, attrs: SftpAttributes): void;
|
|
42
|
+
handle(reqid: number, handle: Buffer): void;
|
|
43
|
+
data(reqid: number, data: Buffer): void;
|
|
44
|
+
name(reqid: number, entries: Array<{
|
|
45
|
+
filename: string;
|
|
46
|
+
longname: string;
|
|
47
|
+
attrs: SftpAttributes;
|
|
48
|
+
}>): void;
|
|
49
|
+
}
|
|
7
50
|
/** Options for {@link SftpMimic} constructor. */
|
|
8
51
|
export interface SftpMimicOptions {
|
|
9
|
-
port
|
|
52
|
+
/** TCP port to bind. Optional — omit if using as a subsystem handler only (no standalone server). */
|
|
53
|
+
port?: number;
|
|
10
54
|
hostname?: string;
|
|
11
55
|
shell?: VirtualShell;
|
|
12
56
|
vfs?: VirtualFileSystem;
|
|
13
57
|
users?: VirtualUserManager;
|
|
14
58
|
}
|
|
15
59
|
export declare class SftpMimic extends EventEmitter {
|
|
16
|
-
port: number;
|
|
60
|
+
port: number | undefined;
|
|
17
61
|
server: SshServer | null;
|
|
18
62
|
private readonly hostname;
|
|
19
63
|
private readonly shell;
|
|
@@ -44,5 +88,11 @@ export declare class SftpMimic extends EventEmitter {
|
|
|
44
88
|
private openHandle;
|
|
45
89
|
private getHandle;
|
|
46
90
|
private closeHandle;
|
|
47
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Attach SFTP handlers to an already-accepted sftp stream.
|
|
93
|
+
* Used internally by {@link SshMimic} for the SFTP subsystem on the SSH port.
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
attachSftpHandlers(sftp: SftpServerStream, authUser: string): void;
|
|
48
97
|
}
|
|
98
|
+
export {};
|
package/dist/SSHMimic/sftp.js
CHANGED
|
@@ -71,6 +71,9 @@ export class SftpMimic extends EventEmitter {
|
|
|
71
71
|
return this.shell?.users ?? this.users;
|
|
72
72
|
}
|
|
73
73
|
async start() {
|
|
74
|
+
if (this.port === undefined) {
|
|
75
|
+
throw new Error("SftpMimic: cannot start — no port configured (this instance is a subsystem handler only)");
|
|
76
|
+
}
|
|
74
77
|
perf.mark("start");
|
|
75
78
|
const privateKey = loadOrCreateHostKey();
|
|
76
79
|
// Ensure VirtualShell is fully initialized before accepting connections
|
|
@@ -271,6 +274,11 @@ export class SftpMimic extends EventEmitter {
|
|
|
271
274
|
closeHandle(handle) {
|
|
272
275
|
this.handles.delete(handle.toString("hex"));
|
|
273
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Attach SFTP handlers to an already-accepted sftp stream.
|
|
279
|
+
* Used internally by {@link SshMimic} for the SFTP subsystem on the SSH port.
|
|
280
|
+
* @internal
|
|
281
|
+
*/
|
|
274
282
|
attachSftpHandlers(sftp, authUser) {
|
|
275
283
|
const getVfs = () => this.getVfs();
|
|
276
284
|
const getUsers = () => this.getUsers();
|
|
@@ -283,8 +291,6 @@ export class SftpMimic extends EventEmitter {
|
|
|
283
291
|
return;
|
|
284
292
|
}
|
|
285
293
|
const openMode = flags;
|
|
286
|
-
const _canRead = Boolean(openMode & OPEN_MODE.READ);
|
|
287
|
-
const _canWrite = Boolean(openMode & OPEN_MODE.WRITE || openMode & OPEN_MODE.APPEND);
|
|
288
294
|
const canCreate = Boolean(openMode & OPEN_MODE.CREAT);
|
|
289
295
|
const shouldTruncate = Boolean(openMode & OPEN_MODE.TRUNC);
|
|
290
296
|
try {
|
|
@@ -337,7 +343,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
337
343
|
sftp.status(reqid, SFTP_STATUS_CODE.EOF);
|
|
338
344
|
return;
|
|
339
345
|
}
|
|
340
|
-
const chunk = entry.buffer.
|
|
346
|
+
const chunk = entry.buffer.subarray(offset, offset + length);
|
|
341
347
|
sftp.data(reqid, chunk);
|
|
342
348
|
});
|
|
343
349
|
sftp.on("WRITE", async (reqid, handle, offset, data) => {
|
|
@@ -36,6 +36,13 @@ import type { InternalDirectoryNode } from "./internalTypes";
|
|
|
36
36
|
* No base64, no JSON. ~27% smaller than the JSON+base64 format for typical VFS trees.
|
|
37
37
|
*/
|
|
38
38
|
export declare function encodeVfs(root: InternalDirectoryNode): Buffer;
|
|
39
|
+
/**
|
|
40
|
+
* Shallow-fork a decoded rootfs tree: creates new InternalDirectoryNode objects
|
|
41
|
+
* (necessary for per-shell write isolation) but shares all InternalFileNode and
|
|
42
|
+
* InternalStubNode references. Safe because file/stub nodes are never mutated in-place
|
|
43
|
+
* — writes replace the parent's children[name] reference with a new node.
|
|
44
|
+
*/
|
|
45
|
+
export declare function forkDirTree(base: InternalDirectoryNode): InternalDirectoryNode;
|
|
39
46
|
/**
|
|
40
47
|
* Deserialise a binary Buffer produced by {@link encodeVfs} back into an
|
|
41
48
|
* InternalDirectoryNode tree. Throws on magic/version mismatch or truncation.
|
|
@@ -162,7 +162,7 @@ class Decoder {
|
|
|
162
162
|
}
|
|
163
163
|
function decodeNode(dec) {
|
|
164
164
|
const type = dec.readUint8();
|
|
165
|
-
const name = dec.readString();
|
|
165
|
+
const name = internName(dec.readString());
|
|
166
166
|
const mode = dec.readUint32();
|
|
167
167
|
const createdAt = dec.readFloat64();
|
|
168
168
|
const updatedAt = dec.readFloat64();
|
|
@@ -194,10 +194,46 @@ function decodeNode(dec) {
|
|
|
194
194
|
updatedAt,
|
|
195
195
|
children,
|
|
196
196
|
_childCount: count,
|
|
197
|
+
_sortedKeys: null,
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
throw new Error(`[VFS binary] Unknown node type: 0x${type.toString(16)}`);
|
|
200
201
|
}
|
|
202
|
+
// String intern pool for node names — avoids duplicate string allocations per decode call.
|
|
203
|
+
// Names like "bin", "etc", "usr", "passwd" appear in every shell's tree.
|
|
204
|
+
const _namePool = new Map();
|
|
205
|
+
function internName(s) {
|
|
206
|
+
const cached = _namePool.get(s);
|
|
207
|
+
if (cached !== undefined)
|
|
208
|
+
return cached;
|
|
209
|
+
_namePool.set(s, s);
|
|
210
|
+
return s;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Shallow-fork a decoded rootfs tree: creates new InternalDirectoryNode objects
|
|
214
|
+
* (necessary for per-shell write isolation) but shares all InternalFileNode and
|
|
215
|
+
* InternalStubNode references. Safe because file/stub nodes are never mutated in-place
|
|
216
|
+
* — writes replace the parent's children[name] reference with a new node.
|
|
217
|
+
*/
|
|
218
|
+
export function forkDirTree(base) {
|
|
219
|
+
const children = Object.create(null);
|
|
220
|
+
for (const name in base.children) {
|
|
221
|
+
const child = base.children[name];
|
|
222
|
+
children[name] = child.type === "directory"
|
|
223
|
+
? forkDirTree(child)
|
|
224
|
+
: child; // shared — file/stub nodes are immutable until replaced
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
type: "directory",
|
|
228
|
+
name: base.name,
|
|
229
|
+
mode: base.mode,
|
|
230
|
+
createdAt: base.createdAt,
|
|
231
|
+
updatedAt: base.updatedAt,
|
|
232
|
+
children,
|
|
233
|
+
_childCount: base._childCount,
|
|
234
|
+
_sortedKeys: base._sortedKeys, // safe to share — sorted list doesn't change for base
|
|
235
|
+
};
|
|
236
|
+
}
|
|
201
237
|
/**
|
|
202
238
|
* Deserialise a binary Buffer produced by {@link encodeVfs} back into an
|
|
203
239
|
* InternalDirectoryNode tree. Throws on magic/version mismatch or truncation.
|
|
@@ -86,6 +86,8 @@ declare class VirtualFileSystem extends EventEmitter {
|
|
|
86
86
|
private _dirty;
|
|
87
87
|
/** Active host-directory mounts: vPath → { hostPath, readOnly } */
|
|
88
88
|
private readonly mounts;
|
|
89
|
+
/** Sorted mounts cache (longest-path-first). Rebuilt lazily on mount/unmount. */
|
|
90
|
+
private _sortedMounts;
|
|
89
91
|
/** True when running in a browser environment (no host FS access). */
|
|
90
92
|
private static readonly isBrowser;
|
|
91
93
|
constructor(options?: VfsOptions);
|
|
@@ -248,6 +250,11 @@ declare class VirtualFileSystem extends EventEmitter {
|
|
|
248
250
|
chmod(targetPath: string, mode: number): void;
|
|
249
251
|
/** Returns metadata for a file or directory. */
|
|
250
252
|
stat(targetPath: string): VfsNodeStats;
|
|
253
|
+
/**
|
|
254
|
+
* Fast type-only check — no Date/string allocation.
|
|
255
|
+
* Use instead of `stat().type` when that's all you need.
|
|
256
|
+
*/
|
|
257
|
+
statType(targetPath: string): "file" | "directory" | null;
|
|
251
258
|
/** Lists direct children names of a directory (sorted). */
|
|
252
259
|
list(dirPath?: string): string[];
|
|
253
260
|
/** Renders ASCII tree view of a directory hierarchy. */
|