saltcorn-samba 0.4.9 → 0.4.10
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 +51 -0
- package/package.json +3 -2
- package/readdir-compat.js +458 -0
- package/smb-client.js +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ 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.10] – 2026-07-05
|
|
8
|
+
|
|
9
|
+
### Fixed – **`0xC0000033` beim tatsächlichen Auflisten (Tree-View, File-Manager) endgültig behoben**
|
|
10
|
+
|
|
11
|
+
Ab 0.4.9 zeigte der Verbindungstest zwar den grünen Erfolgsstatus mit
|
|
12
|
+
gelbem Hinweis, aber im normalen Betrieb (Tree-View, File-Manager,
|
|
13
|
+
Datei-Listen) knallte die Anwendung mit `Samba: QUERY_DIRECTORY failed:
|
|
14
|
+
0xC0000033` — der `stat`-Fallback verhinderte das nur beim Test, nicht
|
|
15
|
+
bei echten Aufrufen.
|
|
16
|
+
|
|
17
|
+
**Ursache:** `smb3-client@0.2.0` sendet in
|
|
18
|
+
[`open/readdir.js`](https://github.com/euricojardim/smb3-client) hart
|
|
19
|
+
kodiert `FileInformationClass = 37`
|
|
20
|
+
(`FileIdBothDirectoryInformation`). Samba 4.23 lehnt diese
|
|
21
|
+
Info-Klasse für bestimmte Verzeichnisse mit
|
|
22
|
+
`STATUS_OBJECT_NAME_INVALID` (`0xC0000033`) ab. `smbclient` und
|
|
23
|
+
Windows-Explorer benutzen `FileBothDirectoryInformation (3)` bzw.
|
|
24
|
+
`FileDirectoryInformation (1)` — die Samba zuverlässig akzeptiert.
|
|
25
|
+
|
|
26
|
+
**Fix:** Neues Modul `readdir-compat.js`. Es macht Folgendes:
|
|
27
|
+
|
|
28
|
+
1. Für jeden `readdir`-Aufruf wird die Directory zuerst per
|
|
29
|
+
`CREATE`+`DIRECTORY_FILE` geöffnet (wie bei smb3-client selbst).
|
|
30
|
+
2. Dann sendet der Wrapper `QUERY_DIRECTORY` in dieser Reihenfolge:
|
|
31
|
+
`FileIdBothDirectoryInformation (37)` →
|
|
32
|
+
`FileBothDirectoryInformation (3)` →
|
|
33
|
+
`FileDirectoryInformation (1)`. Sobald eine Klasse Erfolg meldet,
|
|
34
|
+
wird sie pro Client-Instanz gemerkt (`WeakMap`), damit
|
|
35
|
+
Folgeaufrufe nicht dreimal probieren.
|
|
36
|
+
3. `0xC0000033` und `STATUS_INVALID_INFO_CLASS` lösen den nächsten
|
|
37
|
+
Versuch aus; jeder andere Fehler (fehlender Pfad, Zugriff
|
|
38
|
+
verweigert, Netzwerk) bricht sofort ab und wird an die bestehende
|
|
39
|
+
deutsche Fehlerbehandlung weitergereicht.
|
|
40
|
+
4. Die Rückgabe enthält Größe/`mtime`/`ctime` direkt aus dem
|
|
41
|
+
`QUERY_DIRECTORY`-Response — dadurch entfällt der bisherige
|
|
42
|
+
Per-Entry-`stat`-Fan-Out, was Directory-Listings spürbar
|
|
43
|
+
beschleunigt.
|
|
44
|
+
5. Bei `readdir` bleibt `client.readdir()` als klassischer Fallback
|
|
45
|
+
erhalten, wenn `readdir-compat` an einem *anderen* Fehler scheitert
|
|
46
|
+
— dann greifen die bereits vorhandenen Rewrites
|
|
47
|
+
(„Pfad existiert nicht", Groß-/Kleinschreibungs-Probes).
|
|
48
|
+
|
|
49
|
+
**Warum das überhaupt geht:** `smb3-client` publiziert nur seinen
|
|
50
|
+
`Client`-Konstruktor über die `exports`-Map. Die internen Wire-
|
|
51
|
+
Encoder/Decoder liegen aber als reguläre `.js`-Dateien in
|
|
52
|
+
`dist/`. Der Wrapper importiert sie über `file://`-URLs direkt vom
|
|
53
|
+
Filesystem (also `node_modules/smb3-client/dist/wire/structs/...`),
|
|
54
|
+
was den `exports`-Gate umgeht. Das ist stabil, solange die
|
|
55
|
+
dist-Layout-Konventionen der 0.2.x-Reihe von `smb3-client` erhalten
|
|
56
|
+
bleiben.
|
|
57
|
+
|
|
7
58
|
## [0.4.9] – 2026-07-05
|
|
8
59
|
|
|
9
60
|
### Fixed – **`0xC0000033` beim Auflisten des Basispfads (smb3-client-vs-Samba-4.23-Bug jetzt auch für Basispfade abgefangen)**
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
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": {
|
|
7
7
|
"test": "node test/sanitize.test.js",
|
|
8
|
-
"lint": "node --check index.js && node --check smb-client.js && node --check tree-view.js && node --check filemanager-view.js && node --check pdf-view.js && node --check public/samba-tree.js && node --check public/samba-filemanager.js",
|
|
8
|
+
"lint": "node --check index.js && node --check smb-client.js && node --check readdir-compat.js && node --check tree-view.js && node --check filemanager-view.js && node --check pdf-view.js && node --check public/samba-tree.js && node --check public/samba-filemanager.js",
|
|
9
9
|
"prepublishOnly": "npm run lint && npm test"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"index.js",
|
|
41
41
|
"smb-client.js",
|
|
42
|
+
"readdir-compat.js",
|
|
42
43
|
"tree-view.js",
|
|
43
44
|
"filemanager-view.js",
|
|
44
45
|
"pdf-view.js",
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility layer for smb3-client's readdir on Samba 4.23+.
|
|
3
|
+
*
|
|
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.
|
|
13
|
+
*
|
|
14
|
+
* This module implements `readdirCompat(client, sharePath)` which:
|
|
15
|
+
*
|
|
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.
|
|
25
|
+
*
|
|
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.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
"use strict";
|
|
39
|
+
|
|
40
|
+
// Lazy, cached bundle of the internal modules we need. Loaded on first use.
|
|
41
|
+
let _internalsPromise = null;
|
|
42
|
+
|
|
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");
|
|
52
|
+
|
|
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;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!pkgRoot) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"readdir-compat: cannot locate smb3-client package root on disk"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const dist = path.join(pkgRoot, "dist");
|
|
68
|
+
const urlOf = (rel) =>
|
|
69
|
+
pathToFileURL(path.join(dist, rel)).href;
|
|
70
|
+
|
|
71
|
+
const [
|
|
72
|
+
queryDirectoryMod,
|
|
73
|
+
createMod,
|
|
74
|
+
closeMod,
|
|
75
|
+
commandsMod,
|
|
76
|
+
pathsMod,
|
|
77
|
+
bufferMod,
|
|
78
|
+
] = 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")),
|
|
85
|
+
]);
|
|
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,
|
|
101
|
+
splitSharePath: pathsMod.splitSharePath,
|
|
102
|
+
toSmbPath: pathsMod.toSmbPath,
|
|
103
|
+
Reader: bufferMod.Reader,
|
|
104
|
+
};
|
|
105
|
+
})();
|
|
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
|
+
|
|
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;
|
|
163
|
+
}
|
|
164
|
+
|
|
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
|
+
/**
|
|
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}>>}
|
|
267
|
+
*/
|
|
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
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
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
|
+
|
|
396
|
+
const items = [];
|
|
397
|
+
let first = true;
|
|
398
|
+
for (;;) {
|
|
399
|
+
const body = encodeQueryDirectoryRequest({
|
|
400
|
+
fileInformationClass: cls.code,
|
|
401
|
+
flags: first ? QueryDirectoryFlag.RESTART_SCANS : 0,
|
|
402
|
+
fileIndex: 0,
|
|
403
|
+
fileId,
|
|
404
|
+
searchPattern: first ? "*" : "",
|
|
405
|
+
outputBufferLength: 65536,
|
|
406
|
+
});
|
|
407
|
+
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;
|
|
423
|
+
}
|
|
424
|
+
const buf = decodeQueryDirectoryResponse(resp.body, 64);
|
|
425
|
+
if (buf.length === 0) break;
|
|
426
|
+
const page = cls.parse(buf, Reader);
|
|
427
|
+
for (const e of page) items.push(e);
|
|
428
|
+
if (page.length === 0) break;
|
|
429
|
+
}
|
|
430
|
+
// Filter "." and ".."
|
|
431
|
+
return items.filter((x) => x.fileName !== "." && x.fileName !== "..");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// SMB file attribute bit for directory (MS-FSCC 2.6).
|
|
435
|
+
const FILE_ATTRIBUTE_DIRECTORY = 0x10;
|
|
436
|
+
|
|
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);
|
|
444
|
+
}
|
|
445
|
+
|
|
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
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
module.exports = { readdirCompat };
|
package/smb-client.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
const path = require("path");
|
|
32
|
+
const { readdirCompat } = require("./readdir-compat");
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Dynamic ESM import cache for smb3-client
|
|
@@ -273,6 +274,31 @@ async function buildClient(config) {
|
|
|
273
274
|
*/
|
|
274
275
|
async readdir(rel) {
|
|
275
276
|
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
|
+
}
|
|
276
302
|
let dirents;
|
|
277
303
|
try {
|
|
278
304
|
dirents = await client.readdir(full, { withFileTypes: true });
|