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 +124 -0
- package/package.json +1 -1
- package/readdir-compat.js +238 -406
- package/smb-client.js +37 -79
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
29
|
+
const fs = require("fs");
|
|
30
|
+
const path = require("path");
|
|
31
|
+
const url = require("url");
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
let
|
|
33
|
+
let LOAD_PROMISE = null;
|
|
34
|
+
let INTERNALS = null;
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 (!
|
|
61
|
+
if (!smb3Root) {
|
|
63
62
|
throw new Error(
|
|
64
|
-
"readdir-compat:
|
|
63
|
+
"readdir-compat: smb3-client-Installation nicht gefunden. " +
|
|
64
|
+
"Erwartet in node_modules/smb3-client relativ zu " +
|
|
65
|
+
__dirname
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
bufMod,
|
|
75
|
+
qdMod,
|
|
76
|
+
cmdMod,
|
|
77
|
+
qiMod,
|
|
78
|
+
createStructMod,
|
|
76
79
|
pathsMod,
|
|
77
|
-
|
|
80
|
+
errMod,
|
|
81
|
+
openMod,
|
|
78
82
|
] = await Promise.all([
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
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
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 =
|
|
400
|
-
fileInformationClass:
|
|
401
|
-
|
|
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
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
197
|
+
|
|
198
|
+
const buf = I.decodeQueryDirectoryResponse(resp.body, 64);
|
|
425
199
|
if (buf.length === 0) break;
|
|
426
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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 = {
|
|
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
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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
|
-
|
|
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:
|
|
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.
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
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
|
|
305
|
-
} catch (
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
"
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
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). */
|