saltcorn-samba 0.4.10 → 0.4.12

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,130 @@ 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.12] – 2026-07-06
8
+
9
+ ### Fixed – **`0xC0000033` beim readdir gegen Samba 4.23 endgültig behoben (Root Cause identifiziert)**
10
+
11
+ Nach ausführlicher Wire-Level-Analyse (siehe `smb-diag-report.txt`
12
+ aus 0.4.11) ist die eigentliche Ursache identifiziert:
13
+
14
+ **Bug in `smb3-client@0.2.0`, Datei `dist/wire/structs/queryDirectory.js`:**
15
+ Der Encoder für SMB2 QUERY_DIRECTORY setzt das `FileNameOffset`-Feld
16
+ immer hart auf 96, auch wenn `FileNameLength = 0` gesendet wird
17
+ (z. B. auf der 2. und folgenden Enumeration-Seite).
18
+ [MS-SMB2 §2.2.33](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/10906442-294c-46d3-8515-c277efe1f752)
19
+ verlangt für diesen Fall **`FileNameOffset = 0`**.
20
+ Windows Server toleriert die Fehlbelegung, Samba 4.23 lehnt sie
21
+ strikt mit `STATUS_OBJECT_NAME_INVALID` (`0xC0000033`) ab —
22
+ deshalb funktioniert nichts, was das Listing über mehrere Pages
23
+ braucht, wovon `smb3-client` grundsätzlich ausgeht.
24
+
25
+ ### Fix
26
+
27
+ Die frühere `readdir-compat.js` (die 3 FileInformationClass-Werte
28
+ durchprobierte) ist ersetzt durch einen echten **Wire-Format-Patch**:
29
+
30
+ - Eigener spec-konformer `encodeQueryDirectoryRequest`-Encoder in
31
+ `readdir-compat.js`
32
+ - Eigene QUERY_DIRECTORY-Loop, die den gepatchten Encoder verwendet
33
+ - Wiederverwendung von `Open.withOpen` aus `smb3-client` (Open, Close,
34
+ Tree-Connect bleiben unverändert) via dynamischem ESM-Import über
35
+ `file://` URLs (das `exports`-Gate von `smb3-client` sperrt sonst
36
+ jeden Subpath-Import)
37
+ - `smb-client.js` nutzt jetzt ausschließlich `readdirCompat` als
38
+ readdir-Pfad. Kein Fallback mehr auf das kaputte `client.readdir()`.
39
+ - Bonus: Die Rich-Dirents aus dem gepatchten QUERY_DIRECTORY liefern
40
+ Name, Größe, mtime und ctime in einem Roundtrip. Der bisherige
41
+ Fan-out mit einem `stat()`-Aufruf pro Eintrag entfällt — große
42
+ Verzeichnisse werden dadurch **deutlich schneller**.
43
+
44
+ ### Bekannt – Upstream-Fix eingereicht
45
+
46
+ Parallel wurde ein Bug-Report an `smb3-client` (GitHub:
47
+ `euricojardim/smb3-client`) vorbereitet inklusive Reproduktions-
48
+ Diagnose und Patch-Vorschlag. Sobald der Upstream-Fix in einer
49
+ neuen `smb3-client`-Version verfügbar ist, kann dieses Plugin die
50
+ `readdir-compat.js` wieder entfernen und die eingebaute API direkt
51
+ nutzen.
52
+
53
+ ### Migration – keine Konfigurationsänderung nötig
54
+
55
+ Der Fix greift automatisch. Der gelbe Hinweis „Basispfad bestätigt,
56
+ aber Auflisten funktioniert nicht“ aus 0.4.9–0.4.11 fällt weg, weil
57
+ das Auflisten jetzt tatsächlich funktioniert. Der File-Manager
58
+ zeigt Ordner-Inhalte, die Baum-Ansicht rendert Verzeichnis-Bäume,
59
+ PDF-Ansicht und Datei-Downloads funktionieren unverändert.
60
+
61
+ ---
62
+
63
+ ## [0.4.11] – 2026-07-06
64
+
65
+ ### Changed – **Ehrliche Fehlerpropagation statt stiller Fallback**
66
+
67
+ In 0.4.10 wurde der neue `readdir-compat.js`-Wrapper eingeführt, der
68
+ vor dem eigentlichen `QUERY_DIRECTORY` die FileInformationClass
69
+ automatisch von 37 → 3 → 1 durchprobiert. Zwei Probleme kamen dabei
70
+ zum Vorschein:
71
+
72
+ 1. **Alle drei Info-Klassen lieferten weiterhin `0xC0000033`** auf
73
+ dem betroffenen Server. Das Umschalten der Info-Klasse ist also
74
+ *nicht* die eigentliche Ursache — die Annahme aus 0.4.10 war falsch.
75
+ 2. **Der Wrapper in `smb-client.js` fiel bei totalem Compat-Fehler
76
+ still auf das kaputte `client.readdir()` zurück**, das exakt
77
+ dieselbe (kaputte) FileInformationClass 37 verwendet. Dadurch
78
+ wurde die 0.4.10-Diagnostik komplett verschluckt und das Symptom
79
+ sah unverändert aus wie vor 0.4.10.
80
+
81
+ **Fixes in 0.4.11:**
82
+
83
+ - `smb-client.js` erkennt jetzt den 0xC0000033-Erschöpfungsfehler aus
84
+ dem Compat-Modul (Muster `"all classes exhausted"`) und propagiert
85
+ ihn direkt — kein stiller Retry mehr über das kaputte
86
+ `client.readdir()`.
87
+ - `readdir-compat.js` wirft bei Ausschöpfung aller drei Klassen einen
88
+ synthetischen Fehler mit klarem Wortlaut:
89
+ `"QUERY_DIRECTORY failed: 0xC0000033 (all classes exhausted; no
90
+ working FileInformationClass on this server). Tried:
91
+ FileIdBothDirectoryInformation=37, FileBothDirectoryInformation=3,
92
+ FileDirectoryInformation=1."`
93
+ Der Fehler trägt zusätzlich ein `.attempts`-Array mit den
94
+ NT-Statuscodes pro Klasse für spätere Diagnose.
95
+
96
+ ### Was das für dich bedeutet
97
+
98
+ Der gelbe Hinweis und `QUERY_DIRECTORY failed: 0xC0000033` sind
99
+ **noch nicht behoben** — 0.4.11 macht den Fehler nur ehrlich sichtbar,
100
+ sodass wir aus dem nächsten Diagnoselauf echte Signale bekommen.
101
+
102
+ **Nächster Schritt — bitte auf dem Saltcorn-Server ausführen** (im
103
+ entpackten Plugin-Verzeichnis, oder direkt aus dem ZIP):
104
+
105
+ ```bash
106
+ cd /pfad/zu/saltcorn-samba
107
+ node tools/diag-basepath.js \
108
+ --host 192.168.110.10 \
109
+ --share buero \
110
+ --path static \
111
+ --user 01_vassen \
112
+ --domain buero.ib-vassen.de \
113
+ --password 'DEIN_PASSWORT'
114
+ ```
115
+
116
+ Das Skript führt 6 Sonden mit rohen NT-Statuscodes aus (TREE_CONNECT,
117
+ CREATE, QUERY_DIRECTORY mit allen drei Info-Klassen, `stat`-Probe).
118
+ Die Ausgabe zeigt, an welcher Stelle Samba den Fehler wirft und mit
119
+ welchem Statuscode. Damit können wir 0.4.12 gezielt schreiben statt
120
+ weiter zu raten.
121
+
122
+ Ein heißer Kandidat für 0.4.12 (Wire-Format-Bug in `smb3-client`):
123
+ `encodeQueryDirectoryRequest` setzt `FileNameOffset` immer auf 96,
124
+ auch wenn kein Suchmuster gesendet wird — laut
125
+ [MS-SMB2 §2.2.33](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/10906442-294c-46d3-8515-c277efe1f752)
126
+ MUSS das Feld dann 0 sein. Bestätigung dafür aber bitte erst nach
127
+ der Diagnose.
128
+
129
+ ---
130
+
7
131
  ## [0.4.10] – 2026-07-05
8
132
 
9
133
  ### Fixed – **`0xC0000033` beim tatsächlichen Auflisten (Tree-View, File-Manager) endgültig behoben**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
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": {
package/readdir-compat.js CHANGED
@@ -1,458 +1,290 @@
1
+ "use strict";
2
+
1
3
  /**
2
- * Compatibility layer for smb3-client's readdir on Samba 4.23+.
4
+ * readdir-compat.js — saltcorn-samba 0.4.12
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.
3
8
  *
4
- * Background
5
- * ----------
6
- * `smb3-client@0.2.0` hard-codes `FileInformationClass = 37`
7
- * (`FileIdBothDirectoryInformation`) in its `QUERY_DIRECTORY` request.
8
- * Some Samba 4.23+ configurations reject that class with
9
- * `STATUS_OBJECT_NAME_INVALID` (0xC0000033) for specific directories
10
- * even though the directory can be opened, stat'd and traversed. The
11
- * same directories can be listed by `smbclient` (which requests class 3
12
- * or 1) and by Windows Explorer.
9
+ * Root cause (dokumentiert in tools/diag-basepath.js-Report):
10
+ * smb3-client's `encodeQueryDirectoryRequest` setzt `FileNameOffset`
11
+ * immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird
12
+ * (auf 2. und folgenden Enumeration-Pages).
13
+ * MS-SMB2 §2.2.33 verlangt für diesen Fall FileNameOffset = 0.
14
+ * Windows toleriert die Fehlbelegung, Samba 4.23 nicht.
13
15
  *
14
- * This module implements `readdirCompat(client, sharePath)` which:
16
+ * Strategie:
17
+ * Statt smb3-client's `client.readdir()` / `readdirAll` verwenden wir
18
+ * einen eigenen Loop, der `Open.withOpen` von smb3-client wieder-
19
+ * verwendet (Open, Close, Tree-Connect bleiben unverändert) und nur
20
+ * die kaputte QUERY_DIRECTORY-Loop durch eine spec-konforme ersetzt.
15
21
  *
16
- * 1. Reaches into smb3-client's *internal* wire primitives (via direct
17
- * filesystem imports see NOTE below).
18
- * 2. Opens the directory the same way `client.readdir()` does.
19
- * 3. Sends `QUERY_DIRECTORY` with `FileBothDirectoryInformation (3)`,
20
- * which is the identical layout minus the 8-byte trailing `FileId`
21
- * field.
22
- * 4. Falls back to `FileDirectoryInformation (1)` on the same error.
23
- * 5. Remembers the working class on the given client so subsequent
24
- * readdir calls do not need to retry.
22
+ * Nur die interne wire-Struktur wird via file:// URL importiert, weil
23
+ * smb3-client's `exports`-Gate keine Subpath-Imports zulässt.
25
24
  *
26
- * NOTE subpath imports
27
- * ----------------------
28
- * `smb3-client` publishes with `"exports": { ".": ... }`, so subpath
29
- * imports via package name (`smb3-client/dist/...`) are blocked by
30
- * Node. We work around that by resolving the absolute filesystem path
31
- * of the main entry (via a dynamic `import("smb3-client")` and reading
32
- * `import.meta.url` off the returned URL of a re-export) and then
33
- * constructing `file://` URLs to the sibling internal modules. This
34
- * pattern is stable across `smb3-client 0.2.x` — the internal file
35
- * tree is documented in its README.
25
+ * Idempotent, thread-safe (Node ist single-threaded, aber wir wollen
26
+ * keinen Race auf paralleler Erst-Nutzung).
36
27
  */
37
28
 
38
- "use strict";
29
+ const fs = require("fs");
30
+ const path = require("path");
31
+ const url = require("url");
39
32
 
40
- // Lazy, cached bundle of the internal modules we need. Loaded on first use.
41
- let _internalsPromise = null;
33
+ let LOAD_PROMISE = null;
34
+ let INTERNALS = null;
42
35
 
43
- async function loadInternals() {
44
- if (_internalsPromise) return _internalsPromise;
45
- _internalsPromise = (async () => {
46
- // Locate smb3-client's package root on disk. We do this by walking
47
- // require.resolve.paths() and looking for a real dist/index.js.
48
- // (require.resolve() itself is blocked by the "exports" gate.)
49
- const path = require("path");
50
- const fs = require("fs");
51
- const { pathToFileURL } = require("url");
36
+ /**
37
+ * Interne Wire-/Path-Module aus smb3-client laden. Lazy + gecached.
38
+ * Wirft, wenn smb3-client nicht auffindbar oder Interna verändert.
39
+ */
40
+ function loadInternals() {
41
+ if (INTERNALS) return Promise.resolve(INTERNALS);
42
+ if (LOAD_PROMISE) return LOAD_PROMISE;
52
43
 
53
- let pkgRoot = null;
54
- const candidatePaths = require.resolve.paths("smb3-client") || [];
55
- for (const p of candidatePaths) {
56
- const guess = path.join(p, "smb3-client");
57
- if (fs.existsSync(path.join(guess, "dist", "index.js"))) {
58
- pkgRoot = guess;
44
+ LOAD_PROMISE = (async () => {
45
+ // smb3-client-Root finden (package.json ist via exports-Gate blockiert,
46
+ // wir gehen über den Ordner-Pfad)
47
+ const candidates = [
48
+ path.join(__dirname, "node_modules", "smb3-client"),
49
+ path.join(__dirname, "..", "node_modules", "smb3-client"),
50
+ path.join(__dirname, "..", "..", "node_modules", "smb3-client"),
51
+ path.join(__dirname, "..", "..", "..", "node_modules", "smb3-client"),
52
+ ];
53
+ let smb3Root = null;
54
+ for (const c of candidates) {
55
+ try {
56
+ fs.accessSync(path.join(c, "dist", "index.js"));
57
+ smb3Root = c;
59
58
  break;
60
- }
59
+ } catch (_) {}
61
60
  }
62
- if (!pkgRoot) {
61
+ if (!smb3Root) {
63
62
  throw new Error(
64
- "readdir-compat: cannot locate smb3-client package root on disk"
63
+ "readdir-compat: smb3-client-Installation nicht gefunden. " +
64
+ "Erwartet in node_modules/smb3-client relativ zu " +
65
+ __dirname
65
66
  );
66
67
  }
67
- const dist = path.join(pkgRoot, "dist");
68
- const urlOf = (rel) =>
69
- pathToFileURL(path.join(dist, rel)).href;
68
+
69
+ const distDir = path.join(smb3Root, "dist");
70
+ const load = (rel) =>
71
+ import(url.pathToFileURL(path.join(distDir, rel)).href);
70
72
 
71
73
  const [
72
- queryDirectoryMod,
73
- createMod,
74
- closeMod,
75
- commandsMod,
74
+ bufMod,
75
+ qdMod,
76
+ cmdMod,
77
+ qiMod,
78
+ createStructMod,
76
79
  pathsMod,
77
- bufferMod,
80
+ errMod,
81
+ openMod,
78
82
  ] = await Promise.all([
79
- import(urlOf("wire/structs/queryDirectory.js")),
80
- import(urlOf("wire/structs/create.js")),
81
- import(urlOf("wire/structs/close.js")),
82
- import(urlOf("wire/commands.js")),
83
- import(urlOf("paths.js")),
84
- import(urlOf("wire/buffer.js")),
83
+ load("wire/buffer.js"),
84
+ load("wire/structs/queryDirectory.js"),
85
+ load("wire/commands.js"),
86
+ load("wire/structs/queryInfo.js"),
87
+ load("wire/structs/create.js"),
88
+ load("paths.js"),
89
+ load("errors.js"),
90
+ load("open/open.js"),
85
91
  ]);
86
- return {
87
- encodeQueryDirectoryRequest: queryDirectoryMod.encodeQueryDirectoryRequest,
88
- decodeQueryDirectoryResponse: queryDirectoryMod.decodeQueryDirectoryResponse,
89
- QueryDirectoryFlag: queryDirectoryMod.QueryDirectoryFlag,
90
- encodeCreateRequest: createMod.encodeCreateRequest,
91
- decodeCreateResponse: createMod.decodeCreateResponse,
92
- encodeCloseRequest: closeMod.encodeCloseRequest,
93
- CreateDisposition: createMod.CreateDisposition,
94
- FileAccess: createMod.FileAccess,
95
- FileAttribute: createMod.FileAttribute,
96
- ShareAccess: createMod.ShareAccess,
97
- SmbCommand: commandsMod.SmbCommand,
98
- NTStatus: commandsMod.NTStatus,
99
- isSuccess: commandsMod.isSuccess,
100
- statusName: commandsMod.statusName,
92
+
93
+ if (typeof bufMod.Writer !== "function")
94
+ throw new Error("readdir-compat: Writer-Klasse fehlt");
95
+ if (typeof qdMod.decodeQueryDirectoryResponse !== "function")
96
+ throw new Error("readdir-compat: decodeQueryDirectoryResponse fehlt");
97
+ if (typeof qdMod.parseFileIdBothDirectoryInformation !== "function")
98
+ throw new Error("readdir-compat: parseFileIdBothDirectoryInformation fehlt");
99
+ if (typeof openMod.Open !== "function")
100
+ throw new Error("readdir-compat: Open-Klasse fehlt");
101
+
102
+ INTERNALS = {
103
+ smb3Root,
104
+ Writer: bufMod.Writer,
105
+ decodeQueryDirectoryResponse: qdMod.decodeQueryDirectoryResponse,
106
+ parseFileIdBothDirectoryInformation:
107
+ qdMod.parseFileIdBothDirectoryInformation,
108
+ QueryDirectoryFlag: qdMod.QueryDirectoryFlag,
109
+ SmbCommand: cmdMod.SmbCommand,
110
+ NTStatus: cmdMod.NTStatus,
111
+ isSuccess: cmdMod.isSuccess,
112
+ statusName: cmdMod.statusName,
113
+ FileInformationClass: qiMod.FileInformationClass,
114
+ FileAccess: createStructMod.FileAccess,
115
+ ShareAccess: createStructMod.ShareAccess,
116
+ CreateDisposition: createStructMod.CreateDisposition,
117
+ CreateOptions: createStructMod.CreateOptions,
118
+ FileAttribute: createStructMod.FileAttribute,
101
119
  splitSharePath: pathsMod.splitSharePath,
102
120
  toSmbPath: pathsMod.toSmbPath,
103
- Reader: bufferMod.Reader,
121
+ smbTimeToDate: pathsMod.smbTimeToDate,
122
+ SmbError: errMod.SmbError,
123
+ Open: openMod.Open,
104
124
  };
125
+ return INTERNALS;
105
126
  })();
106
- return _internalsPromise;
107
- }
108
-
109
- // ---------------------------------------------------------------------------
110
- // Parsers for the alternative information classes
111
- // ---------------------------------------------------------------------------
112
- //
113
- // [MS-FSCC] 2.4.10 FileDirectoryInformation:
114
- // NextEntryOffset u32
115
- // FileIndex u32
116
- // CreationTime u64
117
- // LastAccessTime u64
118
- // LastWriteTime u64
119
- // ChangeTime u64
120
- // EndOfFile u64
121
- // AllocationSize u64
122
- // FileAttributes u32
123
- // FileNameLength u32
124
- // FileName wchar[]
125
- //
126
- // [MS-FSCC] 2.4.6 FileBothDirectoryInformation adds after FileNameLength:
127
- // EaSize u32
128
- // ShortNameLength u8
129
- // Reserved1 u8
130
- // ShortName wchar[12] (24 bytes)
131
- // FileName wchar[]
132
127
 
133
- function parseFileDirectoryInformation(buf, Reader) {
134
- const out = [];
135
- let off = 0;
136
- while (off < buf.length) {
137
- const r = new Reader(buf);
138
- r.offset = off;
139
- const next = r.u32();
140
- r.u32(); // FileIndex
141
- const creationTime = r.u64();
142
- const lastAccessTime = r.u64();
143
- const lastWriteTime = r.u64();
144
- const changeTime = r.u64();
145
- const endOfFile = r.u64();
146
- r.u64(); // AllocationSize
147
- const fileAttributes = r.u32();
148
- const fileNameLength = r.u32();
149
- const fileName = fileNameLength > 0 ? r.utf16(fileNameLength) : "";
150
- out.push({
151
- fileName,
152
- endOfFile,
153
- fileAttributes,
154
- creationTime,
155
- lastAccessTime,
156
- lastWriteTime,
157
- changeTime,
158
- });
159
- if (next === 0) break;
160
- off += next;
161
- }
162
- return out;
128
+ return LOAD_PROMISE;
163
129
  }
164
130
 
165
- function parseFileBothDirectoryInformation(buf, Reader) {
166
- const out = [];
167
- let off = 0;
168
- while (off < buf.length) {
169
- const r = new Reader(buf);
170
- r.offset = off;
171
- const next = r.u32();
172
- r.u32(); // FileIndex
173
- const creationTime = r.u64();
174
- const lastAccessTime = r.u64();
175
- const lastWriteTime = r.u64();
176
- const changeTime = r.u64();
177
- const endOfFile = r.u64();
178
- r.u64(); // AllocationSize
179
- const fileAttributes = r.u32();
180
- const fileNameLength = r.u32();
181
- r.u32(); // EaSize
182
- r.u8(); // ShortNameLength
183
- r.u8(); // Reserved1
184
- r.bytes(24); // ShortName
185
- const fileName = fileNameLength > 0 ? r.utf16(fileNameLength) : "";
186
- out.push({
187
- fileName,
188
- endOfFile,
189
- fileAttributes,
190
- creationTime,
191
- lastAccessTime,
192
- lastWriteTime,
193
- changeTime,
194
- });
195
- if (next === 0) break;
196
- off += next;
197
- }
198
- return out;
199
- }
200
-
201
- // FileIdBothDirectoryInformation (37) — same parser as smb3-client
202
- // (used only if smb3-client's own readdir failed for OTHER reasons).
203
- function parseFileIdBothDirectoryInformation(buf, Reader) {
204
- const out = [];
205
- let off = 0;
206
- while (off < buf.length) {
207
- const r = new Reader(buf);
208
- r.offset = off;
209
- const next = r.u32();
210
- r.u32(); // FileIndex
211
- const creationTime = r.u64();
212
- const lastAccessTime = r.u64();
213
- const lastWriteTime = r.u64();
214
- const changeTime = r.u64();
215
- const endOfFile = r.u64();
216
- r.u64(); // AllocationSize
217
- const fileAttributes = r.u32();
218
- const fileNameLength = r.u32();
219
- r.u32(); // EaSize
220
- r.u8(); // ShortNameLength
221
- r.u8(); // Reserved1
222
- r.bytes(24); // ShortName
223
- r.u16(); // Reserved2
224
- r.bytes(8); // FileId
225
- const fileName = fileNameLength > 0 ? r.utf16(fileNameLength) : "";
226
- out.push({
227
- fileName,
228
- endOfFile,
229
- fileAttributes,
230
- creationTime,
231
- lastAccessTime,
232
- lastWriteTime,
233
- changeTime,
234
- });
235
- if (next === 0) break;
236
- off += next;
237
- }
238
- return out;
239
- }
240
-
241
- // ---------------------------------------------------------------------------
242
- // Per-client sticky preferred information class
243
- // ---------------------------------------------------------------------------
244
-
245
- const PREFERRED_CLASS = new WeakMap(); // raw smb3-client Client → number
246
-
247
- // Ordered list of classes we try. First one is smb3-client's default, so
248
- // clients on servers that accept it get zero extra round-trips.
249
- const TRY_ORDER = [
250
- { code: 37, parse: parseFileIdBothDirectoryInformation, name: "FileIdBothDirectoryInformation" },
251
- { code: 3, parse: parseFileBothDirectoryInformation, name: "FileBothDirectoryInformation" },
252
- { code: 1, parse: parseFileDirectoryInformation, name: "FileDirectoryInformation" },
253
- ];
254
-
255
- // ---------------------------------------------------------------------------
256
- // Public entrypoint
257
- // ---------------------------------------------------------------------------
258
-
259
131
  /**
260
- * List a directory using the same open-and-query dance as
261
- * `client.readdir(path)` but with a configurable — and self-fallback —
262
- * `FileInformationClass`.
263
- *
264
- * @param {object} rawClient — instance of smb3-client's `Client`
265
- * @param {string} sharePath — "share/sub/dir" (share = first segment)
266
- * @returns {Promise<Array<{name, isDirectory, size, mtime, birthtime, ctime}>>}
132
+ * Spec-konformer Encoder für SMB2 QUERY_DIRECTORY (MS-SMB2 §2.2.33).
133
+ * Anders als das Original:
134
+ * - FileNameOffset = 0, wenn kein Suchpattern (statt 96)
135
+ * Ansonsten identisch (Struktur + Padding-Byte).
267
136
  */
268
- async function readdirCompat(rawClient, sharePath) {
269
- const int = await loadInternals();
270
- const {
271
- encodeQueryDirectoryRequest,
272
- decodeQueryDirectoryResponse,
273
- QueryDirectoryFlag,
274
- encodeCreateRequest,
275
- decodeCreateResponse,
276
- encodeCloseRequest,
277
- CreateDisposition,
278
- FileAccess,
279
- ShareAccess,
280
- SmbCommand,
281
- NTStatus,
282
- isSuccess,
283
- statusName,
284
- splitSharePath,
285
- toSmbPath,
286
- Reader,
287
- } = int;
288
-
289
- const { share, rest } = splitSharePath(sharePath);
290
- // treeFor is a "private" (TS-private, JS-public) method on Client.
291
- const tree = await rawClient.treeFor(share);
292
-
293
- // Open the target directory with DIRECTORY_FILE createOption (=1),
294
- // matching what smb3-client itself does for readdir.
295
- const createBody = encodeCreateRequest({
296
- filename: toSmbPath(rest),
297
- desiredAccess:
298
- FileAccess.FILE_READ_DATA | FileAccess.FILE_READ_ATTRIBUTES,
299
- shareAccess:
300
- ShareAccess.READ | ShareAccess.WRITE | ShareAccess.DELETE,
301
- createDisposition: CreateDisposition.OPEN,
302
- createOptions: 1, // DIRECTORY_FILE
303
- fileAttributes: 0,
304
- });
305
- const createSigning = tree.session.makeSigning();
306
- const createResp = await tree.conn.send(SmbCommand.CREATE, createBody, {
307
- sessionId: tree.session.sessionId,
308
- treeId: tree.treeId,
309
- ...(createSigning !== undefined ? { signing: createSigning } : {}),
310
- encrypt: tree.encryptRequired,
311
- creditCharge: 1,
312
- });
313
- if (!isSuccess(createResp.header.status)) {
314
- const e = new Error(
315
- "CREATE failed: " + statusName(createResp.header.status)
316
- );
317
- e.status = createResp.header.status;
318
- throw e;
319
- }
320
- const meta = decodeCreateResponse(createResp.body);
321
- const fileId = meta.fileId;
322
-
323
- try {
324
- // Try preferred class first, then the rest in TRY_ORDER.
325
- const preferred = PREFERRED_CLASS.get(rawClient);
326
- const order = preferred !== undefined
327
- ? [
328
- TRY_ORDER.find((c) => c.code === preferred),
329
- ...TRY_ORDER.filter((c) => c.code !== preferred),
330
- ]
331
- : TRY_ORDER.slice();
332
-
333
- let lastErr = null;
334
- for (const cls of order) {
335
- try {
336
- const entries = await queryAll({
337
- tree, fileId, cls,
338
- encodeQueryDirectoryRequest,
339
- decodeQueryDirectoryResponse,
340
- QueryDirectoryFlag,
341
- SmbCommand,
342
- NTStatus,
343
- isSuccess,
344
- statusName,
345
- Reader,
346
- });
347
- // Cache the winning class for the next readdir on this client.
348
- PREFERRED_CLASS.set(rawClient, cls.code);
349
- return entries.map(dirEntryToPluginEntry);
350
- } catch (err) {
351
- lastErr = err;
352
- // Only retry with a different class on OBJECT_NAME_INVALID / info-class
353
- // rejection — any other error (auth, connection, missing) is fatal.
354
- const msg = String((err && err.message) || err || "");
355
- const isInfoClassBug =
356
- /0xC0000033|OBJECT_NAME_INVALID|STATUS_INVALID_INFO_CLASS/i.test(msg);
357
- if (!isInfoClassBug) {
358
- throw err;
359
- }
360
- // else: fall through to next class in order
361
- }
362
- }
363
- throw lastErr;
364
- } finally {
365
- // Always close the handle. Ignore errors.
366
- try {
367
- const closeBody = encodeCloseRequest(fileId);
368
- const closeSigning = tree.session.makeSigning();
369
- await tree.conn.send(SmbCommand.CLOSE, closeBody, {
370
- sessionId: tree.session.sessionId,
371
- treeId: tree.treeId,
372
- ...(closeSigning !== undefined ? { signing: closeSigning } : {}),
373
- encrypt: tree.encryptRequired,
374
- creditCharge: 1,
375
- });
376
- } catch (_) {
377
- // best-effort
378
- }
379
- }
137
+ function encodeQueryDirectoryRequestFixed(I, req) {
138
+ const pat = req.searchPattern
139
+ ? Buffer.from(req.searchPattern, "utf16le")
140
+ : Buffer.alloc(0);
141
+ const w = new I.Writer();
142
+ w.u16(33); // StructureSize
143
+ w.u8(req.fileInformationClass);
144
+ w.u8(req.flags);
145
+ w.u32(req.fileIndex);
146
+ w.bytes(req.fileId); // 16 bytes
147
+ w.u16(pat.length === 0 ? 0 : 64 + 32);
148
+ w.u16(pat.length);
149
+ w.u32(req.outputBufferLength);
150
+ if (pat.length === 0) w.u8(0);
151
+ else w.bytes(pat);
152
+ return w.buffer();
380
153
  }
381
154
 
382
- // Send QUERY_DIRECTORY in a loop until STATUS_NO_MORE_FILES.
383
- async function queryAll(opts) {
384
- const {
385
- tree, fileId, cls,
386
- encodeQueryDirectoryRequest,
387
- decodeQueryDirectoryResponse,
388
- QueryDirectoryFlag,
389
- SmbCommand,
390
- NTStatus,
391
- isSuccess,
392
- statusName,
393
- Reader,
394
- } = opts;
395
-
155
+ /**
156
+ * QUERY_DIRECTORY-Loop mit gepatchtem Encoder.
157
+ * Nachbildung von smb3-client's readdirAll(), aber mit korrektem
158
+ * FileNameOffset auf Folge-Pages.
159
+ */
160
+ async function readdirAllFixed(I, open) {
396
161
  const items = [];
397
162
  let first = true;
398
163
  for (;;) {
399
- const body = encodeQueryDirectoryRequest({
400
- fileInformationClass: cls.code,
401
- flags: first ? QueryDirectoryFlag.RESTART_SCANS : 0,
164
+ const body = encodeQueryDirectoryRequestFixed(I, {
165
+ fileInformationClass:
166
+ I.FileInformationClass.FileIdBothDirectoryInformation,
167
+ flags: first ? I.QueryDirectoryFlag.RESTART_SCANS : 0,
402
168
  fileIndex: 0,
403
- fileId,
169
+ fileId: open.fileId,
404
170
  searchPattern: first ? "*" : "",
405
171
  outputBufferLength: 65536,
406
172
  });
407
173
  first = false;
408
- const signing = tree.session.makeSigning();
409
- const resp = await tree.conn.send(SmbCommand.QUERY_DIRECTORY, body, {
410
- sessionId: tree.session.sessionId,
411
- treeId: tree.treeId,
412
- ...(signing !== undefined ? { signing } : {}),
413
- encrypt: tree.encryptRequired,
414
- creditCharge: 1,
415
- });
416
- if (resp.header.status === NTStatus.STATUS_NO_MORE_FILES) break;
417
- if (!isSuccess(resp.header.status)) {
418
- const err = new Error(
419
- "QUERY_DIRECTORY failed: " + statusName(resp.header.status)
420
- );
421
- err.status = resp.header.status;
422
- throw err;
174
+
175
+ const signing = open.tree.session.makeSigning();
176
+ const resp = await open.tree.conn.send(
177
+ I.SmbCommand.QUERY_DIRECTORY,
178
+ body,
179
+ {
180
+ sessionId: open.tree.session.sessionId,
181
+ treeId: open.tree.treeId,
182
+ ...(signing !== undefined ? { signing } : {}),
183
+ encrypt: open.tree.encryptRequired,
184
+ creditCharge: 1,
185
+ }
186
+ );
187
+
188
+ if (resp.header.status === I.NTStatus.STATUS_NO_MORE_FILES) break;
189
+ if (!I.isSuccess(resp.header.status)) {
190
+ throw new I.SmbError({
191
+ status: resp.header.status,
192
+ message:
193
+ "QUERY_DIRECTORY (compat) failed: " +
194
+ I.statusName(resp.header.status),
195
+ });
423
196
  }
424
- const buf = decodeQueryDirectoryResponse(resp.body, 64);
197
+
198
+ const buf = I.decodeQueryDirectoryResponse(resp.body, 64);
425
199
  if (buf.length === 0) break;
426
- const page = cls.parse(buf, Reader);
200
+
201
+ const page = I.parseFileIdBothDirectoryInformation(buf);
427
202
  for (const e of page) items.push(e);
428
203
  if (page.length === 0) break;
429
204
  }
430
- // Filter "." and ".."
431
205
  return items.filter((x) => x.fileName !== "." && x.fileName !== "..");
432
206
  }
433
207
 
434
- // SMB file attribute bit for directory (MS-FSCC 2.6).
435
- const FILE_ATTRIBUTE_DIRECTORY = 0x10;
208
+ /**
209
+ * Öffentliche API: readdirCompat(client, fullPath, opts)
210
+ *
211
+ * Drop-in-Ersatz für `client.readdir(fullPath, opts)` von smb3-client.
212
+ * fullPath: "share/rest/of/path" (smb3-client-Konvention)
213
+ * opts: { withFileTypes?: boolean }
214
+ *
215
+ * Rückgabe:
216
+ * opts.withFileTypes === true → Dirent[] { name, isFile(), isDirectory() }
217
+ * sonst → string[] Dateinamen
218
+ *
219
+ * Zusätzlich (ergänzend zur smb3-client-API) Rich-Objects auf Wunsch:
220
+ * opts.rich === true → RichDirent[]
221
+ * { name, size, isFile(), isDirectory(), mtime, ctime, atime,
222
+ * changeTime, attributes, hidden, system, readonly, archive }
223
+ */
224
+ async function readdirCompat(client, fullPath, opts) {
225
+ const I = await loadInternals();
226
+
227
+ const { share, rest } = I.splitSharePath(fullPath);
228
+ const tree = await client.treeFor(share);
436
229
 
437
- // SMB time = 100-ns intervals since 1601-01-01. Convert to JS Date.
438
- function smbTimeToDate(u64) {
439
- if (u64 === undefined || u64 === null) return undefined;
440
- // u64 is a BigInt from Reader.u64()
441
- const ms = Number(u64 / 10000n) - 11644473600000;
442
- if (!isFinite(ms)) return undefined;
443
- return new Date(ms);
230
+ return I.Open.withOpen(
231
+ tree,
232
+ {
233
+ filename: I.toSmbPath(rest),
234
+ desiredAccess:
235
+ I.FileAccess.FILE_READ_DATA | I.FileAccess.FILE_READ_ATTRIBUTES,
236
+ shareAccess:
237
+ I.ShareAccess.READ | I.ShareAccess.WRITE | I.ShareAccess.DELETE,
238
+ createDisposition: I.CreateDisposition.OPEN,
239
+ createOptions: I.CreateOptions.DIRECTORY_FILE, // 1
240
+ fileAttributes: 0,
241
+ },
242
+ async (open) => {
243
+ const entries = await readdirAllFixed(I, open);
244
+ if (opts && opts.rich) {
245
+ return entries.map((e) => {
246
+ const attr = e.fileAttributes;
247
+ const isDir = (attr & I.FileAttribute.DIRECTORY) !== 0;
248
+ return {
249
+ name: e.fileName,
250
+ size: Number(e.endOfFile),
251
+ attributes: attr,
252
+ isFile: () => !isDir,
253
+ isDirectory: () => isDir,
254
+ mtime: I.smbTimeToDate(e.lastWriteTime),
255
+ ctime: I.smbTimeToDate(e.creationTime),
256
+ atime: I.smbTimeToDate(e.lastAccessTime),
257
+ changeTime: I.smbTimeToDate(e.changeTime),
258
+ hidden: (attr & 0x02) !== 0,
259
+ system: (attr & 0x04) !== 0,
260
+ readonly: (attr & 0x01) !== 0,
261
+ archive: (attr & 0x20) !== 0,
262
+ };
263
+ });
264
+ }
265
+ if (opts && opts.withFileTypes) {
266
+ return entries.map((e) => {
267
+ const isDir =
268
+ (e.fileAttributes & I.FileAttribute.DIRECTORY) !== 0;
269
+ return {
270
+ name: e.fileName,
271
+ isFile: () => !isDir,
272
+ isDirectory: () => isDir,
273
+ };
274
+ });
275
+ }
276
+ return entries.map((e) => e.fileName);
277
+ }
278
+ );
444
279
  }
445
280
 
446
- function dirEntryToPluginEntry(e) {
447
- const isDir = (e.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) !== 0;
448
- return {
449
- name: e.fileName,
450
- isDirectory: isDir,
451
- size: isDir ? 0 : Number(e.endOfFile || 0n),
452
- mtime: smbTimeToDate(e.lastWriteTime),
453
- birthtime: smbTimeToDate(e.creationTime),
454
- ctime: smbTimeToDate(e.changeTime),
455
- };
281
+ /** Für Diagnose: Vorabladen erzwingen und Interna prüfen. */
282
+ async function ensureLoaded() {
283
+ await loadInternals();
284
+ return { loaded: true, smb3Root: INTERNALS.smb3Root };
456
285
  }
457
286
 
458
- module.exports = { readdirCompat };
287
+ module.exports = {
288
+ readdirCompat,
289
+ ensureLoaded,
290
+ };
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,35 +255,18 @@ async function buildClient(config) {
274
255
  */
275
256
  async readdir(rel) {
276
257
  const full = resolvePath(rel);
277
- // v0.4.10: use the compat readdir which tries multiple
278
- // FileInformationClass values so we survive Samba 4.23's rejection
279
- // of FileIdBothDirectoryInformation (37) the class smb3-client
280
- // hardcodes and which many Samba configurations answer with
281
- // STATUS_OBJECT_NAME_INVALID (0xC0000033). readdirCompat also
282
- // returns rich entries (name+isDir+size+mtime+ctime) directly, so
283
- // we skip the per-entry stat fan-out on success.
284
- let entriesFromCompat = null;
285
- try {
286
- entriesFromCompat = await readdirCompat(client, full);
287
- return entriesFromCompat;
288
- } catch (err) {
289
- // Fall back to smb3-client's built-in readdir with the same error
290
- // handling as before. This preserves prior behaviour for servers
291
- // where the compat path fails for reasons unrelated to the
292
- // FileInformationClass bug (e.g. missing dir, permission).
293
- const compatMsg = String((err && err.message) || err || "");
294
- const compatIsInfoClassOnly =
295
- /STATUS_INVALID_INFO_CLASS/i.test(compatMsg);
296
- if (!compatIsInfoClassOnly) {
297
- // Not an info-class problem — surface below by re-running
298
- // through the classic path so the existing German error
299
- // rewrites still apply.
300
- }
301
- }
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.
302
265
  let dirents;
303
266
  try {
304
- dirents = await client.readdir(full, { withFileTypes: true });
305
- } catch (err) {
267
+ dirents = await readdirCompat(client, full, { rich: true });
268
+ } catch (errCompat) {
269
+ const err = errCompat;
306
270
  // Some Samba builds (observed on 4.20+/4.23+) reject
307
271
  // SMB2 QUERY_DIRECTORY on the *share root* with
308
272
  // STATUS_OBJECT_NAME_INVALID (0xC0000033) because smb3-client
@@ -314,18 +278,20 @@ async function buildClient(config) {
314
278
  // require the caller to configure a real base_path so every
315
279
  // readdir happens inside a directory the server can enumerate.
316
280
  const msg = String((err && err.message) || err || "");
317
- const isRoot = !rel && !basePath;
318
- const isRootEnumBug =
319
- /0xC0000033|OBJECT_NAME_INVALID|QUERY_DIRECTORY/i.test(msg);
320
- 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)) {
321
286
  const e = new Error(
322
- "Das Share-Root lässt sich auf diesem Samba-Server nicht " +
323
- "direkt auflisten (" + (msg || "QUERY_DIRECTORY failed") + "). " +
324
- "Bitte in der Plugin-Config einen Basispfad setzen (z.\u202fB. " +
325
- "einen Unterordner der Freigabe wie „daten“ oder „projekte“) " +
326
- "dann funktionieren alle Directory-Listings innerhalb dieses " +
327
- "Unterordners. Details siehe README, Abschnitt „Troubleshooting → " +
328
- "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
329
295
  );
330
296
  e.cause = err;
331
297
  throw e;
@@ -354,18 +320,10 @@ async function buildClient(config) {
354
320
  }
355
321
  throw err;
356
322
  }
357
- // Parallel enrichment. Bounded to a reasonable concurrency to avoid
358
- // saturating the SMB session on huge directories.
359
- const CHUNK = 16;
360
- const result = [];
361
- for (let i = 0; i < dirents.length; i += CHUNK) {
362
- const slice = dirents.slice(i, i + CHUNK);
363
- const enriched = await Promise.all(
364
- slice.map((d) => enrichEntry(d, full))
365
- );
366
- result.push(...enriched);
367
- }
368
- 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);
369
327
  },
370
328
 
371
329
  /** Stat a single file/directory (name-relative-to-share/basePath). */