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/CHANGELOG.md +101 -0
- package/package.json +2 -1
- package/readdir-compat.js +247 -419
- package/smb-client.js +37 -83
- package/tools/diag-basepath.js +115 -0
- package/tools/diag-wire.js +352 -0
package/readdir-compat.js
CHANGED
|
@@ -1,471 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* readdir-compat.js — saltcorn-samba 0.4.13
|
|
5
|
+
*
|
|
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.
|
|
3
8
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
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
|
+
* BUG 1 (FileNameOffset bei leerem Pattern):
|
|
10
|
+
* smb3-client's `encodeQueryDirectoryRequest` setzt `FileNameOffset`
|
|
11
|
+
* immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird.
|
|
12
|
+
* MS-SMB2 §2.2.33 verlangt für diesen Fall FileNameOffset = 0.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
23
|
+
* Strategie:
|
|
24
|
+
* Statt smb3-client's `client.readdir()` / `readdirAll` verwenden wir
|
|
25
|
+
* einen eigenen Loop, der `Open.withOpen` von smb3-client wieder-
|
|
26
|
+
* verwendet (Open, Close, Tree-Connect bleiben unverändert) und nur
|
|
27
|
+
* die kaputte QUERY_DIRECTORY-Loop durch eine spec-konforme ersetzt.
|
|
25
28
|
*
|
|
26
|
-
*
|
|
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.
|
|
29
|
+
* Nur die interne wire-Struktur wird via file:// URL importiert, weil
|
|
30
|
+
* smb3-client's `exports`-Gate keine Subpath-Imports zulässt.
|
|
36
31
|
*/
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
const fs = require("fs");
|
|
34
|
+
const path = require("path");
|
|
35
|
+
const url = require("url");
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
let
|
|
37
|
+
let LOAD_PROMISE = null;
|
|
38
|
+
let INTERNALS = null;
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const fs = require("fs");
|
|
51
|
-
const { pathToFileURL } = require("url");
|
|
40
|
+
/**
|
|
41
|
+
* Interne Wire-/Path-Module aus smb3-client laden. Lazy + gecached.
|
|
42
|
+
* Wirft, wenn smb3-client nicht auffindbar oder Interna verändert.
|
|
43
|
+
*/
|
|
44
|
+
function loadInternals() {
|
|
45
|
+
if (INTERNALS) return Promise.resolve(INTERNALS);
|
|
46
|
+
if (LOAD_PROMISE) return LOAD_PROMISE;
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
LOAD_PROMISE = (async () => {
|
|
49
|
+
// smb3-client-Root finden (package.json ist via exports-Gate blockiert,
|
|
50
|
+
// wir gehen über den Ordner-Pfad)
|
|
51
|
+
const candidates = [
|
|
52
|
+
path.join(__dirname, "node_modules", "smb3-client"),
|
|
53
|
+
path.join(__dirname, "..", "node_modules", "smb3-client"),
|
|
54
|
+
path.join(__dirname, "..", "..", "node_modules", "smb3-client"),
|
|
55
|
+
path.join(__dirname, "..", "..", "..", "node_modules", "smb3-client"),
|
|
56
|
+
];
|
|
57
|
+
let smb3Root = null;
|
|
58
|
+
for (const c of candidates) {
|
|
59
|
+
try {
|
|
60
|
+
fs.accessSync(path.join(c, "dist", "index.js"));
|
|
61
|
+
smb3Root = c;
|
|
59
62
|
break;
|
|
60
|
-
}
|
|
63
|
+
} catch (_) {}
|
|
61
64
|
}
|
|
62
|
-
if (!
|
|
65
|
+
if (!smb3Root) {
|
|
63
66
|
throw new Error(
|
|
64
|
-
"readdir-compat:
|
|
67
|
+
"readdir-compat: smb3-client-Installation nicht gefunden. " +
|
|
68
|
+
"Erwartet in node_modules/smb3-client relativ zu " +
|
|
69
|
+
__dirname
|
|
65
70
|
);
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
72
|
+
|
|
73
|
+
const distDir = path.join(smb3Root, "dist");
|
|
74
|
+
const load = (rel) =>
|
|
75
|
+
import(url.pathToFileURL(path.join(distDir, rel)).href);
|
|
70
76
|
|
|
71
77
|
const [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
bufMod,
|
|
79
|
+
qdMod,
|
|
80
|
+
cmdMod,
|
|
81
|
+
qiMod,
|
|
82
|
+
createStructMod,
|
|
76
83
|
pathsMod,
|
|
77
|
-
|
|
84
|
+
errMod,
|
|
85
|
+
openMod,
|
|
78
86
|
] = await Promise.all([
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
load("wire/buffer.js"),
|
|
88
|
+
load("wire/structs/queryDirectory.js"),
|
|
89
|
+
load("wire/commands.js"),
|
|
90
|
+
load("wire/structs/queryInfo.js"),
|
|
91
|
+
load("wire/structs/create.js"),
|
|
92
|
+
load("paths.js"),
|
|
93
|
+
load("errors.js"),
|
|
94
|
+
load("open/open.js"),
|
|
85
95
|
]);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
|
|
97
|
+
if (typeof bufMod.Writer !== "function")
|
|
98
|
+
throw new Error("readdir-compat: Writer-Klasse fehlt");
|
|
99
|
+
if (typeof qdMod.decodeQueryDirectoryResponse !== "function")
|
|
100
|
+
throw new Error("readdir-compat: decodeQueryDirectoryResponse fehlt");
|
|
101
|
+
if (typeof qdMod.parseFileIdBothDirectoryInformation !== "function")
|
|
102
|
+
throw new Error("readdir-compat: parseFileIdBothDirectoryInformation fehlt");
|
|
103
|
+
if (typeof openMod.Open !== "function")
|
|
104
|
+
throw new Error("readdir-compat: Open-Klasse fehlt");
|
|
105
|
+
|
|
106
|
+
INTERNALS = {
|
|
107
|
+
smb3Root,
|
|
108
|
+
Writer: bufMod.Writer,
|
|
109
|
+
decodeQueryDirectoryResponse: qdMod.decodeQueryDirectoryResponse,
|
|
110
|
+
parseFileIdBothDirectoryInformation:
|
|
111
|
+
qdMod.parseFileIdBothDirectoryInformation,
|
|
112
|
+
QueryDirectoryFlag: qdMod.QueryDirectoryFlag,
|
|
113
|
+
SmbCommand: cmdMod.SmbCommand,
|
|
114
|
+
NTStatus: cmdMod.NTStatus,
|
|
115
|
+
isSuccess: cmdMod.isSuccess,
|
|
116
|
+
statusName: cmdMod.statusName,
|
|
117
|
+
FileInformationClass: qiMod.FileInformationClass,
|
|
118
|
+
FileAccess: createStructMod.FileAccess,
|
|
119
|
+
ShareAccess: createStructMod.ShareAccess,
|
|
120
|
+
CreateDisposition: createStructMod.CreateDisposition,
|
|
121
|
+
CreateOptions: createStructMod.CreateOptions,
|
|
122
|
+
FileAttribute: createStructMod.FileAttribute,
|
|
101
123
|
splitSharePath: pathsMod.splitSharePath,
|
|
102
124
|
toSmbPath: pathsMod.toSmbPath,
|
|
103
|
-
|
|
125
|
+
smbTimeToDate: pathsMod.smbTimeToDate,
|
|
126
|
+
SmbError: errMod.SmbError,
|
|
127
|
+
Open: openMod.Open,
|
|
104
128
|
};
|
|
129
|
+
return INTERNALS;
|
|
105
130
|
})();
|
|
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
131
|
|
|
133
|
-
|
|
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;
|
|
132
|
+
return LOAD_PROMISE;
|
|
163
133
|
}
|
|
164
134
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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;
|
|
135
|
+
/**
|
|
136
|
+
* Spec-konformer Encoder für SMB2 QUERY_DIRECTORY (MS-SMB2 §2.2.33).
|
|
137
|
+
* Anders als das Original:
|
|
138
|
+
* - FileNameOffset = 0, wenn kein Suchpattern (statt 96)
|
|
139
|
+
* Ansonsten identisch (Struktur + Padding-Byte).
|
|
140
|
+
*/
|
|
141
|
+
function encodeQueryDirectoryRequestFixed(I, req) {
|
|
142
|
+
const pat = req.searchPattern
|
|
143
|
+
? Buffer.from(req.searchPattern, "utf16le")
|
|
144
|
+
: Buffer.alloc(0);
|
|
145
|
+
const w = new I.Writer();
|
|
146
|
+
w.u16(33); // StructureSize
|
|
147
|
+
w.u8(req.fileInformationClass);
|
|
148
|
+
w.u8(req.flags);
|
|
149
|
+
w.u32(req.fileIndex);
|
|
150
|
+
w.bytes(req.fileId); // 16 bytes
|
|
151
|
+
w.u16(pat.length === 0 ? 0 : 64 + 32);
|
|
152
|
+
w.u16(pat.length);
|
|
153
|
+
w.u32(req.outputBufferLength);
|
|
154
|
+
if (pat.length === 0) w.u8(0);
|
|
155
|
+
else w.bytes(pat);
|
|
156
|
+
return w.buffer();
|
|
239
157
|
}
|
|
240
158
|
|
|
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
159
|
/**
|
|
260
|
-
*
|
|
261
|
-
* `client.readdir(path)` but with a configurable — and self-fallback —
|
|
262
|
-
* `FileInformationClass`.
|
|
160
|
+
* QUERY_DIRECTORY-Loop mit gepatchtem Encoder.
|
|
263
161
|
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
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).
|
|
267
168
|
*/
|
|
268
|
-
async function
|
|
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
|
-
const attempts = [];
|
|
335
|
-
for (const cls of order) {
|
|
336
|
-
try {
|
|
337
|
-
const entries = await queryAll({
|
|
338
|
-
tree, fileId, cls,
|
|
339
|
-
encodeQueryDirectoryRequest,
|
|
340
|
-
decodeQueryDirectoryResponse,
|
|
341
|
-
QueryDirectoryFlag,
|
|
342
|
-
SmbCommand,
|
|
343
|
-
NTStatus,
|
|
344
|
-
isSuccess,
|
|
345
|
-
statusName,
|
|
346
|
-
Reader,
|
|
347
|
-
});
|
|
348
|
-
// Cache the winning class for the next readdir on this client.
|
|
349
|
-
PREFERRED_CLASS.set(rawClient, cls.code);
|
|
350
|
-
return entries.map(dirEntryToPluginEntry);
|
|
351
|
-
} catch (err) {
|
|
352
|
-
lastErr = err;
|
|
353
|
-
const msg = String((err && err.message) || err || "");
|
|
354
|
-
attempts.push({ class: cls.name, code: cls.code, error: msg });
|
|
355
|
-
// Only retry with a different class on OBJECT_NAME_INVALID / info-class
|
|
356
|
-
// rejection — any other error (auth, connection, missing) is fatal.
|
|
357
|
-
const isInfoClassBug =
|
|
358
|
-
/0xC0000033|OBJECT_NAME_INVALID|STATUS_INVALID_INFO_CLASS/i.test(msg);
|
|
359
|
-
if (!isInfoClassBug) {
|
|
360
|
-
throw err;
|
|
361
|
-
}
|
|
362
|
-
// else: fall through to next class in order
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
// All classes tried and all failed with 0xC0000033. Report a
|
|
366
|
-
// synthesised error that is uniquely identifiable ("all classes
|
|
367
|
-
// exhausted") so the caller can decide not to fall back to
|
|
368
|
-
// smb3-client's own readdir (which uses class 37 again).
|
|
369
|
-
const finalMsg =
|
|
370
|
-
"QUERY_DIRECTORY failed: 0xC0000033 (all classes exhausted; " +
|
|
371
|
-
"no working FileInformationClass on this server). Tried: " +
|
|
372
|
-
attempts.map((a) => a.class + "=" + a.code).join(", ") + ".";
|
|
373
|
-
const summary = new Error(finalMsg);
|
|
374
|
-
summary.cause = lastErr;
|
|
375
|
-
summary.attempts = attempts;
|
|
376
|
-
throw summary;
|
|
377
|
-
} finally {
|
|
378
|
-
// Always close the handle. Ignore errors.
|
|
379
|
-
try {
|
|
380
|
-
const closeBody = encodeCloseRequest(fileId);
|
|
381
|
-
const closeSigning = tree.session.makeSigning();
|
|
382
|
-
await tree.conn.send(SmbCommand.CLOSE, closeBody, {
|
|
383
|
-
sessionId: tree.session.sessionId,
|
|
384
|
-
treeId: tree.treeId,
|
|
385
|
-
...(closeSigning !== undefined ? { signing: closeSigning } : {}),
|
|
386
|
-
encrypt: tree.encryptRequired,
|
|
387
|
-
creditCharge: 1,
|
|
388
|
-
});
|
|
389
|
-
} catch (_) {
|
|
390
|
-
// best-effort
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Send QUERY_DIRECTORY in a loop until STATUS_NO_MORE_FILES.
|
|
396
|
-
async function queryAll(opts) {
|
|
397
|
-
const {
|
|
398
|
-
tree, fileId, cls,
|
|
399
|
-
encodeQueryDirectoryRequest,
|
|
400
|
-
decodeQueryDirectoryResponse,
|
|
401
|
-
QueryDirectoryFlag,
|
|
402
|
-
SmbCommand,
|
|
403
|
-
NTStatus,
|
|
404
|
-
isSuccess,
|
|
405
|
-
statusName,
|
|
406
|
-
Reader,
|
|
407
|
-
} = opts;
|
|
408
|
-
|
|
169
|
+
async function readdirAllFixed(I, open) {
|
|
409
170
|
const items = [];
|
|
410
171
|
let first = true;
|
|
411
172
|
for (;;) {
|
|
412
|
-
const body =
|
|
413
|
-
fileInformationClass:
|
|
414
|
-
|
|
173
|
+
const body = encodeQueryDirectoryRequestFixed(I, {
|
|
174
|
+
fileInformationClass:
|
|
175
|
+
I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
176
|
+
flags: first ? I.QueryDirectoryFlag.RESTART_SCANS : 0,
|
|
415
177
|
fileIndex: 0,
|
|
416
|
-
fileId,
|
|
417
|
-
searchPattern:
|
|
178
|
+
fileId: open.fileId,
|
|
179
|
+
searchPattern: "*", // IMMER "*", nie "" — Samba lehnt leer ab
|
|
418
180
|
outputBufferLength: 65536,
|
|
419
181
|
});
|
|
420
182
|
first = false;
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
183
|
+
|
|
184
|
+
const signing = open.tree.session.makeSigning();
|
|
185
|
+
const resp = await open.tree.conn.send(
|
|
186
|
+
I.SmbCommand.QUERY_DIRECTORY,
|
|
187
|
+
body,
|
|
188
|
+
{
|
|
189
|
+
sessionId: open.tree.session.sessionId,
|
|
190
|
+
treeId: open.tree.treeId,
|
|
191
|
+
...(signing !== undefined ? { signing } : {}),
|
|
192
|
+
encrypt: open.tree.encryptRequired,
|
|
193
|
+
creditCharge: 1,
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (resp.header.status === I.NTStatus.STATUS_NO_MORE_FILES) break;
|
|
198
|
+
if (!I.isSuccess(resp.header.status)) {
|
|
199
|
+
throw new I.SmbError({
|
|
200
|
+
status: resp.header.status,
|
|
201
|
+
message:
|
|
202
|
+
"QUERY_DIRECTORY (compat) failed: " +
|
|
203
|
+
I.statusName(resp.header.status),
|
|
204
|
+
});
|
|
436
205
|
}
|
|
437
|
-
|
|
206
|
+
|
|
207
|
+
const buf = I.decodeQueryDirectoryResponse(resp.body, 64);
|
|
438
208
|
if (buf.length === 0) break;
|
|
439
|
-
|
|
209
|
+
|
|
210
|
+
const page = I.parseFileIdBothDirectoryInformation(buf);
|
|
440
211
|
for (const e of page) items.push(e);
|
|
441
212
|
if (page.length === 0) break;
|
|
442
213
|
}
|
|
443
|
-
// Filter "." and ".."
|
|
444
214
|
return items.filter((x) => x.fileName !== "." && x.fileName !== "..");
|
|
445
215
|
}
|
|
446
216
|
|
|
447
|
-
|
|
448
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Öffentliche API: readdirCompat(client, fullPath, opts)
|
|
219
|
+
*
|
|
220
|
+
* Drop-in-Ersatz für `client.readdir(fullPath, opts)` von smb3-client.
|
|
221
|
+
* fullPath: "share/rest/of/path" (smb3-client-Konvention)
|
|
222
|
+
* opts: { withFileTypes?: boolean }
|
|
223
|
+
*
|
|
224
|
+
* Rückgabe:
|
|
225
|
+
* opts.withFileTypes === true → Dirent[] { name, isFile(), isDirectory() }
|
|
226
|
+
* sonst → string[] Dateinamen
|
|
227
|
+
*
|
|
228
|
+
* Zusätzlich (ergänzend zur smb3-client-API) Rich-Objects auf Wunsch:
|
|
229
|
+
* opts.rich === true → RichDirent[]
|
|
230
|
+
* { name, size, isFile(), isDirectory(), mtime, ctime, atime,
|
|
231
|
+
* changeTime, attributes, hidden, system, readonly, archive }
|
|
232
|
+
*/
|
|
233
|
+
async function readdirCompat(client, fullPath, opts) {
|
|
234
|
+
const I = await loadInternals();
|
|
235
|
+
|
|
236
|
+
const { share, rest } = I.splitSharePath(fullPath);
|
|
237
|
+
const tree = await client.treeFor(share);
|
|
449
238
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
239
|
+
return I.Open.withOpen(
|
|
240
|
+
tree,
|
|
241
|
+
{
|
|
242
|
+
filename: I.toSmbPath(rest),
|
|
243
|
+
desiredAccess:
|
|
244
|
+
I.FileAccess.FILE_READ_DATA | I.FileAccess.FILE_READ_ATTRIBUTES,
|
|
245
|
+
shareAccess:
|
|
246
|
+
I.ShareAccess.READ | I.ShareAccess.WRITE | I.ShareAccess.DELETE,
|
|
247
|
+
createDisposition: I.CreateDisposition.OPEN,
|
|
248
|
+
createOptions: I.CreateOptions.DIRECTORY_FILE, // 1
|
|
249
|
+
fileAttributes: 0,
|
|
250
|
+
},
|
|
251
|
+
async (open) => {
|
|
252
|
+
const entries = await readdirAllFixed(I, open);
|
|
253
|
+
if (opts && opts.rich) {
|
|
254
|
+
return entries.map((e) => {
|
|
255
|
+
const attr = e.fileAttributes;
|
|
256
|
+
const isDir = (attr & I.FileAttribute.DIRECTORY) !== 0;
|
|
257
|
+
return {
|
|
258
|
+
name: e.fileName,
|
|
259
|
+
size: Number(e.endOfFile),
|
|
260
|
+
attributes: attr,
|
|
261
|
+
isFile: () => !isDir,
|
|
262
|
+
isDirectory: () => isDir,
|
|
263
|
+
mtime: I.smbTimeToDate(e.lastWriteTime),
|
|
264
|
+
ctime: I.smbTimeToDate(e.creationTime),
|
|
265
|
+
atime: I.smbTimeToDate(e.lastAccessTime),
|
|
266
|
+
changeTime: I.smbTimeToDate(e.changeTime),
|
|
267
|
+
hidden: (attr & 0x02) !== 0,
|
|
268
|
+
system: (attr & 0x04) !== 0,
|
|
269
|
+
readonly: (attr & 0x01) !== 0,
|
|
270
|
+
archive: (attr & 0x20) !== 0,
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (opts && opts.withFileTypes) {
|
|
275
|
+
return entries.map((e) => {
|
|
276
|
+
const isDir =
|
|
277
|
+
(e.fileAttributes & I.FileAttribute.DIRECTORY) !== 0;
|
|
278
|
+
return {
|
|
279
|
+
name: e.fileName,
|
|
280
|
+
isFile: () => !isDir,
|
|
281
|
+
isDirectory: () => isDir,
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return entries.map((e) => e.fileName);
|
|
286
|
+
}
|
|
287
|
+
);
|
|
457
288
|
}
|
|
458
289
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
isDirectory: isDir,
|
|
464
|
-
size: isDir ? 0 : Number(e.endOfFile || 0n),
|
|
465
|
-
mtime: smbTimeToDate(e.lastWriteTime),
|
|
466
|
-
birthtime: smbTimeToDate(e.creationTime),
|
|
467
|
-
ctime: smbTimeToDate(e.changeTime),
|
|
468
|
-
};
|
|
290
|
+
/** Für Diagnose: Vorabladen erzwingen und Interna prüfen. */
|
|
291
|
+
async function ensureLoaded() {
|
|
292
|
+
await loadInternals();
|
|
293
|
+
return { loaded: true, smb3Root: INTERNALS.smb3Root };
|
|
469
294
|
}
|
|
470
295
|
|
|
471
|
-
module.exports = {
|
|
296
|
+
module.exports = {
|
|
297
|
+
readdirCompat,
|
|
298
|
+
ensureLoaded,
|
|
299
|
+
};
|