saltcorn-samba 0.4.12 → 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/CHANGELOG.md CHANGED
@@ -4,6 +4,51 @@ All notable changes to `saltcorn-samba` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.4.13] – 2026-07-06
8
+
9
+ ### Fixed – **QUERY_DIRECTORY leere Patterns → 0xC0000033 (die eigentliche Ursache)**
10
+
11
+ Mit v0.4.12 wurde ein Wire-Bug in smb3-client (FileNameOffset) gefixt,
12
+ aber Samba lehnte weiterhin ab. Mit dem neuen `tools/diag-wire.js` konnte
13
+ die tatsächliche Ursache byteweise verifiziert werden:
14
+
15
+ **Bug:** smb3-client's `readdirAll` sendet ab der 2. Enumeration-Seite
16
+ `searchPattern=""` (leerer String). Windows toleriert das, aber Samba's
17
+ `source3/smbd/smb2_query_directory.c` enthält den strikten Check:
18
+
19
+ ```c
20
+ if (state->in_file_name[0] == '\0') {
21
+ tevent_req_nterror(req, NT_STATUS_OBJECT_NAME_INVALID);
22
+ return tevent_req_post(req, ev);
23
+ }
24
+ ```
25
+
26
+ Das ergibt exakt den STATUS_OBJECT_NAME_INVALID (0xC0000033), den wir
27
+ seit v0.4.0 sehen.
28
+
29
+ **Fix in `readdir-compat.js`:** Der eigene Enumeration-Loop sendet auf
30
+ **jeder** Seite `searchPattern="*"`, nicht nur beim ersten Request.
31
+ `RESTART_SCANS` wird nur beim ersten Request gesetzt; ab dann sendet
32
+ Samba nach dem letzten Batch korrekt STATUS_NO_MORE_FILES, was den Loop
33
+ sauber terminiert. Verifiziert via `diag-wire.js`:
34
+
35
+ - Probe 1 (pat=`*`, RESTART): STATUS_SUCCESS, 8 Einträge
36
+ - Probe 2 (pat=`*`, ohne RESTART): STATUS_NO_MORE_FILES → Loop-Ende
37
+ - Probe 6 (pat=`""`, offset=0): **0xC0000033** ← der Bug
38
+
39
+ ### Added
40
+
41
+ - `tools/diag-wire.js` – Wire-Level-Diagnose mit Hex-Dump der SMB2-Bytes,
42
+ testet 6 verschiedene QUERY_DIRECTORY-Varianten (Info-Class, Buffer-
43
+ Größe, mit/ohne Pattern). Nützlich für zukünftige Kompatibilitäts-
44
+ probleme mit anderen SMB-Servern.
45
+
46
+ ### Upstream-Report aktualisiert
47
+
48
+ `smb3-client-bug-report.md` beschreibt jetzt beide Bugs (FileNameOffset
49
+ **und** leeres Pattern gegen Samba). Beide Fixes sind identisch simpel:
50
+ auf jeder Page `*` senden.
51
+
7
52
  ## [0.4.12] – 2026-07-06
8
53
 
9
54
  ### Fixed – **`0xC0000033` beim readdir gegen Samba 4.23 endgültig behoben (Root Cause identifiziert)**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share via SMB 3.1.1 (AES-CMAC signing, optional encryption). File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -44,6 +44,7 @@
44
44
  "filemanager-view.js",
45
45
  "pdf-view.js",
46
46
  "public/",
47
+ "tools/",
47
48
  "README.md",
48
49
  "CHANGELOG.md",
49
50
  "LICENSE"
package/readdir-compat.js CHANGED
@@ -1,17 +1,24 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * readdir-compat.js — saltcorn-samba 0.4.12
4
+ * readdir-compat.js — saltcorn-samba 0.4.13
5
5
  *
6
- * Behebt einen QUERY_DIRECTORY-Wire-Format-Bug in smb3-client@0.2.0 der
7
- * gegen Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033) führt.
6
+ * Behebt zwei QUERY_DIRECTORY-Wire-Bugs in smb3-client@0.2.0 die
7
+ * gegen Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033) führen.
8
8
  *
9
- * Root cause (dokumentiert in tools/diag-basepath.js-Report):
9
+ * BUG 1 (FileNameOffset bei leerem Pattern):
10
10
  * smb3-client's `encodeQueryDirectoryRequest` setzt `FileNameOffset`
11
- * immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird
12
- * (auf 2. und folgenden Enumeration-Pages).
11
+ * immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird.
13
12
  * MS-SMB2 §2.2.33 verlangt für diesen Fall FileNameOffset = 0.
14
- * Windows toleriert die Fehlbelegung, Samba 4.23 nicht.
13
+ *
14
+ * BUG 2 (Leeres Pattern auf Folge-Pages): ← Der Hauptbug!
15
+ * smb3-client sendet ab der 2. Enumeration-Page `searchPattern=""`.
16
+ * Windows toleriert das, Samba 4.23 lehnt es strikt ab. Belegt via
17
+ * diag-wire.js: `smb2_query_directory.c` prüft `in_file_name[0] ==
18
+ * '\0'` und antwortet dann mit STATUS_OBJECT_NAME_INVALID.
19
+ * Fix: Auf jeder Page `*` senden. RESTART_SCANS nur beim ersten Mal.
20
+ * Samba beendet die Enumeration dann korrekt mit
21
+ * STATUS_NO_MORE_FILES auf einem Folge-Request ohne RESTART_SCANS.
15
22
  *
16
23
  * Strategie:
17
24
  * Statt smb3-client's `client.readdir()` / `readdirAll` verwenden wir
@@ -21,9 +28,6 @@
21
28
  *
22
29
  * Nur die interne wire-Struktur wird via file:// URL importiert, weil
23
30
  * smb3-client's `exports`-Gate keine Subpath-Imports zulässt.
24
- *
25
- * Idempotent, thread-safe (Node ist single-threaded, aber wir wollen
26
- * keinen Race auf paralleler Erst-Nutzung).
27
31
  */
28
32
 
29
33
  const fs = require("fs");
@@ -154,8 +158,13 @@ function encodeQueryDirectoryRequestFixed(I, req) {
154
158
 
155
159
  /**
156
160
  * QUERY_DIRECTORY-Loop mit gepatchtem Encoder.
157
- * Nachbildung von smb3-client's readdirAll(), aber mit korrektem
158
- * FileNameOffset auf Folge-Pages.
161
+ *
162
+ * Unterschiede zu smb3-client's readdirAll():
163
+ * 1. Auf Folge-Pages wird `searchPattern="*"` gesendet (nicht "").
164
+ * Samba lehnt leere Patterns strikt ab (STATUS_OBJECT_NAME_INVALID).
165
+ * 2. RESTART_SCANS nur beim ersten Request.
166
+ * 3. Encoder setzt FileNameOffset=0 bei leerem Pattern (Fallback für
167
+ * andere kaputte Server, wird hier faktisch nie benutzt).
159
168
  */
160
169
  async function readdirAllFixed(I, open) {
161
170
  const items = [];
@@ -167,7 +176,7 @@ async function readdirAllFixed(I, open) {
167
176
  flags: first ? I.QueryDirectoryFlag.RESTART_SCANS : 0,
168
177
  fileIndex: 0,
169
178
  fileId: open.fileId,
170
- searchPattern: first ? "*" : "",
179
+ searchPattern: "*", // IMMER "*", nie "" — Samba lehnt leer ab
171
180
  outputBufferLength: 65536,
172
181
  });
173
182
  first = false;
@@ -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
+ });