saltcorn-samba 0.4.11 → 0.4.13

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/smb-client.js CHANGED
@@ -224,38 +224,19 @@ async function buildClient(config) {
224
224
  }
225
225
 
226
226
  /**
227
- * Convert a smb3-client Dirent + best-effort stat into the shape the
228
- * rest of the plugin expects (`name`, `isDirectory` as function OR
229
- * boolean, `size`, `mtime`, `birthtime`).
230
- *
231
- * We fetch stat data in parallel because smb3-client's readdir Dirent
232
- * only carries name/isFile/isDirectory. For very large directories this
233
- * would fan out into many stat calls — that's an acceptable trade-off
234
- * for now; a future release can optimise by using SMB2_QUERY_DIRECTORY's
235
- * FileBothDirectoryInformation output directly.
227
+ * Convert a readdirCompat rich-Dirent (which already carries name,
228
+ * size, mtime, ctime, and isDirectory()) directly to the legacy
229
+ * `@marsaud/smb2`-compatible shape the rest of the plugin expects.
230
+ * No extra stat roundtrip needed — v0.4.12 gets attributes straight
231
+ * from the QUERY_DIRECTORY response.
236
232
  */
237
- async function enrichEntry(dirent, parentFullPath) {
238
- const isDir = !!dirent.isDirectory();
239
- const fullPath = parentFullPath
240
- ? parentFullPath + "/" + dirent.name
241
- : dirent.name;
242
- let size = 0;
243
- let mtime;
244
- let birthtime;
245
- try {
246
- const st = await client.stat(fullPath);
247
- size = Number(st.size || 0);
248
- mtime = st.mtime;
249
- birthtime = st.ctime;
250
- } catch (_) {
251
- // Non-fatal; return whatever we already have.
252
- }
233
+ function mapDirent(dirent) {
253
234
  return {
254
235
  name: dirent.name,
255
- isDirectory: isDir,
256
- size,
257
- mtime,
258
- birthtime,
236
+ isDirectory: !!(dirent.isDirectory && dirent.isDirectory()),
237
+ size: Number(dirent.size || 0),
238
+ mtime: dirent.mtime,
239
+ birthtime: dirent.ctime,
259
240
  };
260
241
  }
261
242
 
@@ -274,39 +255,18 @@ async function buildClient(config) {
274
255
  */
275
256
  async readdir(rel) {
276
257
  const full = resolvePath(rel);
277
- // v0.4.11: prefer the compat readdir (which tries multiple
278
- // FileInformationClass values). If it succeeds we are done — no
279
- // extra per-entry stat fan-out needed because compat returns rich
280
- // entries directly. If it fails, we ONLY fall back to smb3-client's
281
- // built-in readdir when the compat failure was not itself the
282
- // Samba-4.23 bug (0xC0000033 across all classes). If it *was*, the
283
- // built-in readdir will produce the exact same 0xC0000033 — no
284
- // point trying it, and it would hide the diagnostics compat has
285
- // gathered. Instead we rethrow the compat error verbatim so the
286
- // caller sees the true root cause.
287
- let compatErr = null;
288
- try {
289
- return await readdirCompat(client, full);
290
- } catch (err) {
291
- compatErr = err;
292
- const compatMsg = String((err && err.message) || err || "");
293
- // If compat ran into the exact same info-class rejection with
294
- // ALL classes exhausted, the built-in readdir cannot possibly
295
- // help — it uses class 37, which compat already tried. Rethrow
296
- // now so callers see the real reason.
297
- if (/0xC0000033|OBJECT_NAME_INVALID/i.test(compatMsg) &&
298
- /all classes exhausted|no working FileInformationClass/i.test(compatMsg)) {
299
- throw err;
300
- }
301
- // Otherwise (compat failed for structural reasons like an unknown
302
- // internal-module path, or a transient socket error) fall through
303
- // to the classic path so the existing German error rewrites still
304
- // apply.
305
- }
258
+ // v0.4.12: readdirCompat ist der ALLEINIGE readdir-Pfad. Grund:
259
+ // smb3-client@0.2.0 hat einen Wire-Format-Bug in
260
+ // encodeQueryDirectoryRequest, der gegen Samba 4.23 zu
261
+ // STATUS_OBJECT_NAME_INVALID (0xC0000033) führt. readdirCompat
262
+ // implementiert QUERY_DIRECTORY MS-SMB2-§2.2.33-konform und
263
+ // umgeht damit den Bug. Es gibt keinen Fallback auf
264
+ // client.readdir(), weil das den bekannten Bug hervorruft.
306
265
  let dirents;
307
266
  try {
308
- dirents = await client.readdir(full, { withFileTypes: true });
309
- } catch (err) {
267
+ dirents = await readdirCompat(client, full, { rich: true });
268
+ } catch (errCompat) {
269
+ const err = errCompat;
310
270
  // Some Samba builds (observed on 4.20+/4.23+) reject
311
271
  // SMB2 QUERY_DIRECTORY on the *share root* with
312
272
  // STATUS_OBJECT_NAME_INVALID (0xC0000033) because smb3-client
@@ -318,18 +278,20 @@ async function buildClient(config) {
318
278
  // require the caller to configure a real base_path so every
319
279
  // readdir happens inside a directory the server can enumerate.
320
280
  const msg = String((err && err.message) || err || "");
321
- const isRoot = !rel && !basePath;
322
- const isRootEnumBug =
323
- /0xC0000033|OBJECT_NAME_INVALID|QUERY_DIRECTORY/i.test(msg);
324
- if (isRoot && isRootEnumBug) {
281
+ // Falls der Compat-Pfad selbst mit 0xC0000033 zurückkommt,
282
+ // liegt entweder ein weiterer, unbekannter Wire-Bug vor oder
283
+ // Samba lehnt aus einem anderen Grund ab. Wir geben eine
284
+ // gezielte Meldung mit Hinweis auf den Bug-Report.
285
+ if (/0xC0000033|OBJECT_NAME_INVALID/i.test(msg)) {
325
286
  const e = new Error(
326
- "Das Share-Root lässt sich auf diesem Samba-Server nicht " +
327
- "direkt auflisten (" + (msg || "QUERY_DIRECTORY failed") + "). " +
328
- "Bitte in der Plugin-Config einen Basispfad setzen (z.\u202fB. " +
329
- "einen Unterordner der Freigabe wie „daten“ oder „projekte“) " +
330
- "dann funktionieren alle Directory-Listings innerhalb dieses " +
331
- "Unterordners. Details siehe README, Abschnitt „Troubleshooting → " +
332
- "QUERY_DIRECTORY auf Share-Root“."
287
+ "Samba lehnt QUERY_DIRECTORY auf diesem Pfad ab " +
288
+ "(STATUS_OBJECT_NAME_INVALID, 0xC0000033). " +
289
+ "Der bekannte smb3-client-Wire-Bug wurde in v0.4.12 gefixt — " +
290
+ "falls dieser Fehler weiterhin auftritt, liegt ein zweiter, " +
291
+ "noch unbekannter Wire-Format-Fehler vor. Bitte " +
292
+ "tools/diag-basepath.js ausführen und Report zurückschicken. " +
293
+ "Details: " +
294
+ msg
333
295
  );
334
296
  e.cause = err;
335
297
  throw e;
@@ -358,18 +320,10 @@ async function buildClient(config) {
358
320
  }
359
321
  throw err;
360
322
  }
361
- // Parallel enrichment. Bounded to a reasonable concurrency to avoid
362
- // saturating the SMB session on huge directories.
363
- const CHUNK = 16;
364
- const result = [];
365
- for (let i = 0; i < dirents.length; i += CHUNK) {
366
- const slice = dirents.slice(i, i + CHUNK);
367
- const enriched = await Promise.all(
368
- slice.map((d) => enrichEntry(d, full))
369
- );
370
- result.push(...enriched);
371
- }
372
- return result;
323
+ // v0.4.12: readdirCompat gibt schon Rich-Dirents zurück (Name,
324
+ // Größe, mtime, ctime, isDirectory). Kein zusätzlicher stat-Fan-out
325
+ // nötig ein einziger QUERY_DIRECTORY-Roundtrip liefert alles.
326
+ return dirents.map(mapDirent);
373
327
  },
374
328
 
375
329
  /** Stat a single file/directory (name-relative-to-share/basePath). */
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone diagnostic script for saltcorn-samba.
4
+ * Runs a series of individual SMB3 operations against a share so we can see
5
+ * exactly which primitive fails and why. Prints raw NT status codes.
6
+ *
7
+ * Usage (from the plugin directory, so smb3-client resolves):
8
+ *
9
+ * node tools/diag-basepath.js \
10
+ * --host 192.168.110.10 --share buero --user 01_vassen \
11
+ * --domain buero.ib-vassen.de --password '…' \
12
+ * --path static
13
+ *
14
+ * The script never writes; it only opens handles.
15
+ */
16
+
17
+ "use strict";
18
+
19
+ async function main() {
20
+ const args = parseArgs(process.argv.slice(2));
21
+ const required = ["host", "share", "user", "path"];
22
+ for (const k of required) {
23
+ if (!args[k]) {
24
+ console.error("missing required --" + k);
25
+ process.exit(2);
26
+ }
27
+ }
28
+ const password = args.password || process.env.SMB_PASSWORD || "";
29
+ if (!password) {
30
+ console.error("missing --password (or SMB_PASSWORD env var)");
31
+ process.exit(2);
32
+ }
33
+
34
+ const { Client } = await import("smb3-client");
35
+
36
+ const client = new Client({
37
+ host: args.host,
38
+ port: Number(args.port) || 445,
39
+ domain: args.domain || "",
40
+ username: args.user,
41
+ password,
42
+ connectTimeout: 10_000,
43
+ requestTimeout: 30_000,
44
+ signing: args.signing || "if-offered",
45
+ encryption: args.encryption || "if-offered",
46
+ });
47
+
48
+ console.log("[1/7] Connecting …");
49
+ await client.connect();
50
+ console.log(" \u2713 TCP + Negotiate + Session-Setup + Auth OK");
51
+
52
+ const share = args.share;
53
+ const path = args.path;
54
+ const shareAndPath = share + "/" + path;
55
+ console.log("[2/7] TREE_CONNECT to \\\\" + args.host + "\\" + share + " \u2026");
56
+ try {
57
+ // Trigger TREE_CONNECT by touching the raw internal — but smb3-client
58
+ // does that implicitly on the first per-share call. So instead we call
59
+ // a cheap op below and let the connect surface the error there.
60
+ console.log(" (deferred until first op)");
61
+ } catch (e) {
62
+ console.error(" \u2717 TREE_CONNECT failed:", e && e.message);
63
+ }
64
+
65
+ // --- Probes ------------------------------------------------------------
66
+ const probes = [
67
+ { label: "readdir(share) \u2014 SHARE ROOT", op: () => client.readdir(share) },
68
+ { label: "readdir(share/'') \u2014 empty subpath", op: () => client.readdir(share + "/") },
69
+ { label: "stat(share/path) \u2014 target as-is", op: () => client.stat(shareAndPath) },
70
+ { label: "readdir(share/path) \u2014 target as-is", op: () => client.readdir(shareAndPath) },
71
+ { label: "readdir(share/PATH) \u2014 target uppercased", op: () => client.readdir(share + "/" + path.toUpperCase()) },
72
+ { label: "readdir(share/path.lc) \u2014 target lowercased", op: () => client.readdir(share + "/" + path.toLowerCase()) },
73
+ ];
74
+
75
+ let step = 3;
76
+ for (const p of probes) {
77
+ console.log("[" + step + "/7] " + p.label);
78
+ try {
79
+ const r = await p.op();
80
+ const preview = Array.isArray(r)
81
+ ? " (" + r.length + " entries" +
82
+ (r.length ? ", first: " +
83
+ JSON.stringify(r.slice(0, 3).map((e) => (typeof e === "string" ? e : e.name))) : "") + ")"
84
+ : " (" + JSON.stringify(r).slice(0, 120) + ")";
85
+ console.log(" \u2713 OK" + preview);
86
+ } catch (e) {
87
+ console.log(" \u2717 " + (e && e.message ? e.message.split("\n")[0] : String(e)));
88
+ if (e && e.status) console.log(" NT status: 0x" + e.status.toString(16));
89
+ if (e && e.code) console.log(" error code: " + e.code);
90
+ }
91
+ step++;
92
+ }
93
+
94
+ await client.disconnect();
95
+ console.log("Done.");
96
+ }
97
+
98
+ function parseArgs(argv) {
99
+ const out = {};
100
+ for (let i = 0; i < argv.length; i++) {
101
+ const a = argv[i];
102
+ if (a.startsWith("--")) {
103
+ const key = a.slice(2);
104
+ const nxt = argv[i + 1];
105
+ if (!nxt || nxt.startsWith("--")) { out[key] = true; }
106
+ else { out[key] = nxt; i++; }
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+
112
+ main().catch((e) => {
113
+ console.error("FATAL:", e && e.stack || e);
114
+ process.exit(1);
115
+ });
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * diag-wire.js — Wire-level QUERY_DIRECTORY diagnostic.
4
+ *
5
+ * Opens a connection, tree-connects, opens the target directory, then sends
6
+ * a hand-built QUERY_DIRECTORY packet with FULL HEX DUMP of both request
7
+ * and response. This bypasses smb3-client's encoders so we can compare bytes.
8
+ *
9
+ * Usage (from the plugin directory):
10
+ * node tools/diag-wire.js \
11
+ * --host 192.168.110.10 --share buero --user 01_vassen \
12
+ * --domain buero.ib-vassen.de --password '...' \
13
+ * --path static
14
+ */
15
+
16
+ "use strict";
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+ const url = require("url");
21
+
22
+ async function loadInternals() {
23
+ const candidates = [
24
+ path.join(__dirname, "..", "node_modules", "smb3-client"),
25
+ path.join(__dirname, "..", "..", "node_modules", "smb3-client"),
26
+ ];
27
+ let smb3Root = null;
28
+ for (const c of candidates) {
29
+ try {
30
+ fs.accessSync(path.join(c, "dist", "index.js"));
31
+ smb3Root = c;
32
+ break;
33
+ } catch (_) {}
34
+ }
35
+ if (!smb3Root) {
36
+ throw new Error("smb3-client not found in node_modules");
37
+ }
38
+ const distDir = path.join(smb3Root, "dist");
39
+ const load = (rel) => import(url.pathToFileURL(path.join(distDir, rel)).href);
40
+
41
+ const [
42
+ idx,
43
+ bufMod,
44
+ qdMod,
45
+ cmdMod,
46
+ qiMod,
47
+ createStructMod,
48
+ pathsMod,
49
+ errMod,
50
+ openMod,
51
+ ] = await Promise.all([
52
+ load("index.js"),
53
+ load("wire/buffer.js"),
54
+ load("wire/structs/queryDirectory.js"),
55
+ load("wire/commands.js"),
56
+ load("wire/structs/queryInfo.js"),
57
+ load("wire/structs/create.js"),
58
+ load("paths.js"),
59
+ load("errors.js"),
60
+ load("open/open.js"),
61
+ ]);
62
+
63
+ return {
64
+ Client: idx.Client,
65
+ Writer: bufMod.Writer,
66
+ encodeQueryDirectoryRequest: qdMod.encodeQueryDirectoryRequest,
67
+ decodeQueryDirectoryResponse: qdMod.decodeQueryDirectoryResponse,
68
+ parseFileIdBothDirectoryInformation: qdMod.parseFileIdBothDirectoryInformation,
69
+ QueryDirectoryFlag: qdMod.QueryDirectoryFlag,
70
+ SmbCommand: cmdMod.SmbCommand,
71
+ NTStatus: cmdMod.NTStatus,
72
+ isSuccess: cmdMod.isSuccess,
73
+ statusName: cmdMod.statusName,
74
+ FileInformationClass: qiMod.FileInformationClass,
75
+ FileAccess: createStructMod.FileAccess,
76
+ ShareAccess: createStructMod.ShareAccess,
77
+ CreateDisposition: createStructMod.CreateDisposition,
78
+ CreateOptions: createStructMod.CreateOptions,
79
+ splitSharePath: pathsMod.splitSharePath,
80
+ toSmbPath: pathsMod.toSmbPath,
81
+ SmbError: errMod.SmbError,
82
+ Open: openMod.Open,
83
+ };
84
+ }
85
+
86
+ function hex(buf, label) {
87
+ console.log(`\n--- ${label} (${buf.length} bytes) ---`);
88
+ for (let off = 0; off < buf.length; off += 16) {
89
+ const slice = buf.slice(off, Math.min(off + 16, buf.length));
90
+ const hexPart = Array.from(slice)
91
+ .map((b) => b.toString(16).padStart(2, "0"))
92
+ .join(" ")
93
+ .padEnd(48);
94
+ const asciiPart = Array.from(slice)
95
+ .map((b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : "."))
96
+ .join("");
97
+ console.log(
98
+ `${off.toString(16).padStart(4, "0")} ${hexPart} ${asciiPart}`
99
+ );
100
+ }
101
+ }
102
+
103
+ // Build QUERY_DIRECTORY body byte-by-byte with named fields
104
+ function buildQdBody(I, opts) {
105
+ const {
106
+ fileInformationClass,
107
+ flags,
108
+ fileIndex,
109
+ fileId,
110
+ searchPattern,
111
+ outputBufferLength,
112
+ forceFileNameOffsetZero, // testing variant
113
+ } = opts;
114
+
115
+ const pat = Buffer.from(searchPattern, "utf16le");
116
+ const w = new I.Writer();
117
+ w.u16(33); // StructureSize
118
+ w.u8(fileInformationClass);
119
+ w.u8(flags);
120
+ w.u32(fileIndex);
121
+ w.bytes(fileId); // 16
122
+ const fileNameOffset = forceFileNameOffsetZero
123
+ ? 0
124
+ : pat.length === 0
125
+ ? 0
126
+ : 64 + 32;
127
+ w.u16(fileNameOffset);
128
+ w.u16(pat.length);
129
+ w.u32(outputBufferLength);
130
+ if (pat.length === 0) {
131
+ w.u8(0); // pad byte
132
+ } else {
133
+ w.bytes(pat);
134
+ }
135
+ return w.buffer();
136
+ }
137
+
138
+ async function sendRaw(open, I, opts, label) {
139
+ const body = buildQdBody(I, opts);
140
+ hex(body, `${label} REQUEST BODY`);
141
+ console.log(
142
+ `Fields: StructSize=33, FileInformationClass=${opts.fileInformationClass}, ` +
143
+ `Flags=${opts.flags}, FileNameLength=${
144
+ Buffer.from(opts.searchPattern, "utf16le").length
145
+ }, ` +
146
+ `OutputBufferLength=${opts.outputBufferLength}`
147
+ );
148
+ const fnOff = body.readUInt16LE(32 - 8);
149
+ console.log(`FileNameOffset (from body): ${fnOff}`);
150
+
151
+ const signing = open.tree.session.makeSigning();
152
+ try {
153
+ const resp = await open.tree.conn.send(I.SmbCommand.QUERY_DIRECTORY, body, {
154
+ sessionId: open.tree.session.sessionId,
155
+ treeId: open.tree.treeId,
156
+ ...(signing !== undefined ? { signing } : {}),
157
+ encrypt: open.tree.encryptRequired,
158
+ creditCharge: 1,
159
+ });
160
+ console.log(
161
+ `RESPONSE status: 0x${resp.header.status.toString(16)} (${I.statusName(
162
+ resp.header.status
163
+ )})`
164
+ );
165
+ hex(resp.body.slice(0, Math.min(resp.body.length, 128)), `${label} RESPONSE BODY (first 128)`);
166
+ if (I.isSuccess(resp.header.status)) {
167
+ const buf = I.decodeQueryDirectoryResponse(resp.body, 64);
168
+ const items = I.parseFileIdBothDirectoryInformation(buf);
169
+ console.log(
170
+ `Decoded ${items.length} entries: ${items.slice(0, 5).map((e) => e.fileName).join(", ")}`
171
+ );
172
+ }
173
+ return resp;
174
+ } catch (e) {
175
+ console.log(`SEND ERROR: ${e.message}`);
176
+ if (e.status) console.log(` NT status: 0x${e.status.toString(16)}`);
177
+ return null;
178
+ }
179
+ }
180
+
181
+ async function main() {
182
+ const args = parseArgs(process.argv.slice(2));
183
+ const required = ["host", "share", "user", "path"];
184
+ for (const k of required) {
185
+ if (!args[k]) {
186
+ console.error(`missing required --${k}`);
187
+ process.exit(2);
188
+ }
189
+ }
190
+ const password = args.password || process.env.SMB_PASSWORD || "";
191
+ if (!password) {
192
+ console.error("missing --password");
193
+ process.exit(2);
194
+ }
195
+
196
+ const I = await loadInternals();
197
+
198
+ const client = new I.Client({
199
+ host: args.host,
200
+ port: Number(args.port) || 445,
201
+ domain: args.domain || "",
202
+ username: args.user,
203
+ password,
204
+ connectTimeout: 10000,
205
+ requestTimeout: 30000,
206
+ signing: args.signing || "if-offered",
207
+ encryption: args.encryption || "if-offered",
208
+ });
209
+
210
+ console.log("Connecting…");
211
+ await client.connect();
212
+ console.log("Connected");
213
+
214
+ const fullPath = `${args.share}/${args.path}`;
215
+ const { share, rest } = I.splitSharePath(fullPath);
216
+ const tree = await client.treeFor(share);
217
+ console.log(`Tree connected: treeId=${tree.treeId}`);
218
+
219
+ // Open the target directory
220
+ const open = await I.Open.create(tree, {
221
+ filename: I.toSmbPath(rest),
222
+ desiredAccess:
223
+ I.FileAccess.FILE_READ_DATA | I.FileAccess.FILE_READ_ATTRIBUTES,
224
+ shareAccess: I.ShareAccess.READ | I.ShareAccess.WRITE | I.ShareAccess.DELETE,
225
+ createDisposition: I.CreateDisposition.OPEN,
226
+ createOptions: I.CreateOptions.DIRECTORY_FILE,
227
+ fileAttributes: 0,
228
+ });
229
+ console.log(`Opened dir, fileId hex: ${open.fileId.toString("hex")}`);
230
+
231
+ // ---------------- 6 probes ----------------
232
+ console.log("\n============ PROBE 1: pat='*', flags=RESTART, class=FileIdBothDir ============");
233
+ await sendRaw(
234
+ open,
235
+ I,
236
+ {
237
+ fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
238
+ flags: I.QueryDirectoryFlag.RESTART_SCANS,
239
+ fileIndex: 0,
240
+ fileId: open.fileId,
241
+ searchPattern: "*",
242
+ outputBufferLength: 65536,
243
+ },
244
+ "P1"
245
+ );
246
+
247
+ console.log("\n============ PROBE 2: pat='*', flags=0, class=FileIdBothDir ============");
248
+ await sendRaw(
249
+ open,
250
+ I,
251
+ {
252
+ fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
253
+ flags: 0,
254
+ fileIndex: 0,
255
+ fileId: open.fileId,
256
+ searchPattern: "*",
257
+ outputBufferLength: 65536,
258
+ },
259
+ "P2"
260
+ );
261
+
262
+ console.log("\n============ PROBE 3: pat='*', flags=RESTART, class=FileBothDir (0x03) ============");
263
+ await sendRaw(
264
+ open,
265
+ I,
266
+ {
267
+ fileInformationClass: I.FileInformationClass.FileBothDirectoryInformation ?? 3,
268
+ flags: I.QueryDirectoryFlag.RESTART_SCANS,
269
+ fileIndex: 0,
270
+ fileId: open.fileId,
271
+ searchPattern: "*",
272
+ outputBufferLength: 65536,
273
+ },
274
+ "P3"
275
+ );
276
+
277
+ console.log("\n============ PROBE 4: pat='*', flags=RESTART, class=FileDirInfo (0x01) ============");
278
+ await sendRaw(
279
+ open,
280
+ I,
281
+ {
282
+ fileInformationClass: I.FileInformationClass.FileDirectoryInformation ?? 1,
283
+ flags: I.QueryDirectoryFlag.RESTART_SCANS,
284
+ fileIndex: 0,
285
+ fileId: open.fileId,
286
+ searchPattern: "*",
287
+ outputBufferLength: 65536,
288
+ },
289
+ "P4"
290
+ );
291
+
292
+ console.log("\n============ PROBE 5: pat='*', outBuf=8192, flags=RESTART ============");
293
+ await sendRaw(
294
+ open,
295
+ I,
296
+ {
297
+ fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
298
+ flags: I.QueryDirectoryFlag.RESTART_SCANS,
299
+ fileIndex: 0,
300
+ fileId: open.fileId,
301
+ searchPattern: "*",
302
+ outputBufferLength: 8192,
303
+ },
304
+ "P5"
305
+ );
306
+
307
+ console.log("\n============ PROBE 6: pat='', flags=RESTART (empty pattern, offset=0) ============");
308
+ await sendRaw(
309
+ open,
310
+ I,
311
+ {
312
+ fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
313
+ flags: I.QueryDirectoryFlag.RESTART_SCANS,
314
+ fileIndex: 0,
315
+ fileId: open.fileId,
316
+ searchPattern: "",
317
+ outputBufferLength: 65536,
318
+ },
319
+ "P6"
320
+ );
321
+
322
+ console.log("\nAvailable FileInformationClass values:");
323
+ console.log(JSON.stringify(I.FileInformationClass, null, 2));
324
+
325
+ try {
326
+ await open.close();
327
+ } catch (_) {}
328
+ console.log("\nDone.");
329
+ }
330
+
331
+ function parseArgs(argv) {
332
+ const out = {};
333
+ for (let i = 0; i < argv.length; i++) {
334
+ const a = argv[i];
335
+ if (a.startsWith("--")) {
336
+ const key = a.slice(2);
337
+ const nxt = argv[i + 1];
338
+ if (!nxt || nxt.startsWith("--")) {
339
+ out[key] = true;
340
+ } else {
341
+ out[key] = nxt;
342
+ i++;
343
+ }
344
+ }
345
+ }
346
+ return out;
347
+ }
348
+
349
+ main().catch((e) => {
350
+ console.error("FATAL:", (e && e.stack) || e);
351
+ process.exit(1);
352
+ });