saltcorn-samba 0.4.12 → 0.4.14
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 +89 -0
- package/index.js +9 -12
- package/package.json +2 -1
- package/readdir-compat.js +40 -18
- package/smb-client.js +22 -31
- package/tools/diag-basepath.js +118 -0
- package/tools/diag-wire.js +355 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,95 @@ 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.14] – 2026-07-06
|
|
8
|
+
|
|
9
|
+
### Changed – Code-Review / Aufräumen (keine Verhaltensänderung)
|
|
10
|
+
|
|
11
|
+
Interne QS-Runde nach dem v0.4.13-Fix. Der Bug war behoben, aber die
|
|
12
|
+
Kommentare, Fehlertexte und die Fallback-Logik im Connection-Test
|
|
13
|
+
verwiesen noch auf die alte, mittlerweile widerlegte Hypothese
|
|
14
|
+
(`FileInformationClass=37 hart kodiert`). Alle diese Stellen wurden
|
|
15
|
+
auf den tatsächlichen Root Cause aktualisiert bzw. gelöscht.
|
|
16
|
+
|
|
17
|
+
**readdir-compat.js:**
|
|
18
|
+
- Docstring priorisiert den tatsächlichen Hauptbug (leeres Pattern
|
|
19
|
+
auf Folge-Pages) und schiebt den FileNameOffset-Bug in die Sekundär-
|
|
20
|
+
rolle (defensiv abgefangen, wird bei aktueller Loop nie getriggert).
|
|
21
|
+
- Encoder-Docstring erklärt, dass der `pat.length === 0`-Zweig nur
|
|
22
|
+
noch defensive Sicherheit ist.
|
|
23
|
+
|
|
24
|
+
**smb-client.js:**
|
|
25
|
+
- Der lange, spekulative Kommentar-Block über „Some Samba builds
|
|
26
|
+
reject QUERY_DIRECTORY on the share root because FileInformation-
|
|
27
|
+
Class=37 is hardcoded“ wurde entfernt. Der wahre Grund steht jetzt
|
|
28
|
+
kurz und präzise dort.
|
|
29
|
+
- Die 0xC0000033-Fehlermeldung verweist nicht mehr auf v0.4.12 als Fix
|
|
30
|
+
und nicht mehr auf `tools/diag-basepath.js`, sondern auf
|
|
31
|
+
`tools/diag-wire.js` (das aussagekräftigere Tool).
|
|
32
|
+
|
|
33
|
+
**index.js (Connection-Test-Route):**
|
|
34
|
+
- Der „gelber Hinweis“-Fallback bleibt als Safety Net, wird aber nur
|
|
35
|
+
noch aktiv, wenn `readdir` fehlschlägt und `stat` erfolgreich ist —
|
|
36
|
+
ein Zustand, der seit v0.4.13 nicht mehr auftreten sollte.
|
|
37
|
+
- Der Hinweistext sagt jetzt „unerwarteter Zustand, bitte diag-wire.js
|
|
38
|
+
ausführen“ statt der alten, jetzt falschen Erklärung mit
|
|
39
|
+
`FileInformationClass=37`.
|
|
40
|
+
|
|
41
|
+
**tools/diag-basepath.js:**
|
|
42
|
+
- FIX: `client.disconnect()` → `client.close()`. smb3-client's Client
|
|
43
|
+
hat `close()`, nicht `disconnect()` (das war die marsaud-API). Der
|
|
44
|
+
Aufruf schlug bisher am Ende jedes Diagnostic-Runs mit einer
|
|
45
|
+
TypeError-Meldung fehl, ohne aber das Resultat zu beeinträchtigen.
|
|
46
|
+
|
|
47
|
+
**tools/diag-wire.js:**
|
|
48
|
+
- FIX: Zusätzlich zu `open.close()` wird jetzt auch `client.close()`
|
|
49
|
+
aufgerufen, damit der TCP-Socket sauber geschlossen wird.
|
|
50
|
+
|
|
51
|
+
## [0.4.13] – 2026-07-06
|
|
52
|
+
|
|
53
|
+
### Fixed – **QUERY_DIRECTORY leere Patterns → 0xC0000033 (die eigentliche Ursache)**
|
|
54
|
+
|
|
55
|
+
Mit v0.4.12 wurde ein Wire-Bug in smb3-client (FileNameOffset) gefixt,
|
|
56
|
+
aber Samba lehnte weiterhin ab. Mit dem neuen `tools/diag-wire.js` konnte
|
|
57
|
+
die tatsächliche Ursache byteweise verifiziert werden:
|
|
58
|
+
|
|
59
|
+
**Bug:** smb3-client's `readdirAll` sendet ab der 2. Enumeration-Seite
|
|
60
|
+
`searchPattern=""` (leerer String). Windows toleriert das, aber Samba's
|
|
61
|
+
`source3/smbd/smb2_query_directory.c` enthält den strikten Check:
|
|
62
|
+
|
|
63
|
+
```c
|
|
64
|
+
if (state->in_file_name[0] == '\0') {
|
|
65
|
+
tevent_req_nterror(req, NT_STATUS_OBJECT_NAME_INVALID);
|
|
66
|
+
return tevent_req_post(req, ev);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Das ergibt exakt den STATUS_OBJECT_NAME_INVALID (0xC0000033), den wir
|
|
71
|
+
seit v0.4.0 sehen.
|
|
72
|
+
|
|
73
|
+
**Fix in `readdir-compat.js`:** Der eigene Enumeration-Loop sendet auf
|
|
74
|
+
**jeder** Seite `searchPattern="*"`, nicht nur beim ersten Request.
|
|
75
|
+
`RESTART_SCANS` wird nur beim ersten Request gesetzt; ab dann sendet
|
|
76
|
+
Samba nach dem letzten Batch korrekt STATUS_NO_MORE_FILES, was den Loop
|
|
77
|
+
sauber terminiert. Verifiziert via `diag-wire.js`:
|
|
78
|
+
|
|
79
|
+
- Probe 1 (pat=`*`, RESTART): STATUS_SUCCESS, 8 Einträge
|
|
80
|
+
- Probe 2 (pat=`*`, ohne RESTART): STATUS_NO_MORE_FILES → Loop-Ende
|
|
81
|
+
- Probe 6 (pat=`""`, offset=0): **0xC0000033** ← der Bug
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- `tools/diag-wire.js` – Wire-Level-Diagnose mit Hex-Dump der SMB2-Bytes,
|
|
86
|
+
testet 6 verschiedene QUERY_DIRECTORY-Varianten (Info-Class, Buffer-
|
|
87
|
+
Größe, mit/ohne Pattern). Nützlich für zukünftige Kompatibilitäts-
|
|
88
|
+
probleme mit anderen SMB-Servern.
|
|
89
|
+
|
|
90
|
+
### Upstream-Report aktualisiert
|
|
91
|
+
|
|
92
|
+
`smb3-client-bug-report.md` beschreibt jetzt beide Bugs (FileNameOffset
|
|
93
|
+
**und** leeres Pattern gegen Samba). Beide Fixes sind identisch simpel:
|
|
94
|
+
auf jeder Page `*` senden.
|
|
95
|
+
|
|
7
96
|
## [0.4.12] – 2026-07-06
|
|
8
97
|
|
|
9
98
|
### Fixed – **`0xC0000033` beim readdir gegen Samba 4.23 endgültig behoben (Root Cause identifiziert)**
|
package/index.js
CHANGED
|
@@ -1019,21 +1019,18 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
1019
1019
|
})),
|
|
1020
1020
|
truncated: Array.isArray(listing) && listing.length > 20,
|
|
1021
1021
|
note: rootNotEnum
|
|
1022
|
-
? "Verbindung + Anmeldung erfolgreich
|
|
1023
|
-
"
|
|
1024
|
-
"
|
|
1025
|
-
"
|
|
1026
|
-
"
|
|
1022
|
+
? "Verbindung + Anmeldung erfolgreich, aber der Share-Root konnte " +
|
|
1023
|
+
"nicht aufgelistet werden (stat OK, readdir schlug fehl). Das ist " +
|
|
1024
|
+
"ein unerwarteter Zustand \u2014 seit v0.4.13 sollte readdir gegen " +
|
|
1025
|
+
"Samba fehlerfrei laufen. Bitte tools/diag-wire.js ausf\u00fchren " +
|
|
1026
|
+
"und den Report melden."
|
|
1027
1027
|
: baseNotEnum
|
|
1028
1028
|
? "Verbindung + Anmeldung erfolgreich, Basispfad \u201e" +
|
|
1029
1029
|
(listing._basePathForDisplay || testCfg.base_path || "") +
|
|
1030
|
-
"\u201c wurde vom Server best\u00e4tigt (stat OK)
|
|
1031
|
-
"Auflisten
|
|
1032
|
-
"
|
|
1033
|
-
"
|
|
1034
|
-
"und PDF-Ansicht funktionieren \u2014 nur die Wurzel des " +
|
|
1035
|
-
"Basispfads kann nicht direkt aufgelistet werden. Workaround: " +
|
|
1036
|
-
"tieferen Basispfad setzen oder Unterordner ansprechen."
|
|
1030
|
+
"\u201c wurde vom Server best\u00e4tigt (stat OK), das direkte " +
|
|
1031
|
+
"Auflisten schlug aber fehl. Das ist ein unerwarteter Zustand \u2014 " +
|
|
1032
|
+
"seit v0.4.13 sollte readdir gegen Samba fehlerfrei laufen. Bitte " +
|
|
1033
|
+
"tools/diag-wire.js ausf\u00fchren und den Report melden."
|
|
1037
1034
|
: undefined,
|
|
1038
1035
|
});
|
|
1039
1036
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
4
4
|
"description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share via SMB 3.1.1 (AES-CMAC signing, optional encryption). File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"filemanager-view.js",
|
|
45
45
|
"pdf-view.js",
|
|
46
46
|
"public/",
|
|
47
|
+
"tools/",
|
|
47
48
|
"README.md",
|
|
48
49
|
"CHANGELOG.md",
|
|
49
50
|
"LICENSE"
|
package/readdir-compat.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* readdir-compat.js — saltcorn-samba 0.4.
|
|
4
|
+
* readdir-compat.js — saltcorn-samba 0.4.14
|
|
5
5
|
*
|
|
6
|
-
* Behebt
|
|
7
|
-
* gegen Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033)
|
|
6
|
+
* Behebt zwei QUERY_DIRECTORY-Wire-Bugs in smb3-client@0.2.0 die
|
|
7
|
+
* gegen Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033) führen.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* BUG 1 (leeres Pattern auf Folge-Pages) ─ Hauptursache des 0xC0000033:
|
|
10
|
+
* smb3-client's `readdirAll` sendet ab der 2. Enumeration-Page
|
|
11
|
+
* `searchPattern=""`. Windows toleriert das, Samba 4.23 lehnt es
|
|
12
|
+
* strikt ab: `source3/smbd/smb2_query_directory.c` prüft
|
|
13
|
+
* `in_file_name[0] == '\0'` und antwortet mit
|
|
14
|
+
* STATUS_OBJECT_NAME_INVALID.
|
|
15
|
+
* Fix (in readdirAllFixed): Auf jeder Page `*` senden. RESTART_SCANS
|
|
16
|
+
* nur beim ersten Request. Samba beendet nach der letzten Seite
|
|
17
|
+
* sauber mit STATUS_NO_MORE_FILES.
|
|
18
|
+
*
|
|
19
|
+
* BUG 2 (FileNameOffset bei leerem Pattern) ─ defensiv abgefangen:
|
|
10
20
|
* smb3-client's `encodeQueryDirectoryRequest` setzt `FileNameOffset`
|
|
11
|
-
* immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird
|
|
12
|
-
* (auf 2. und folgenden Enumeration-Pages).
|
|
21
|
+
* immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird.
|
|
13
22
|
* MS-SMB2 §2.2.33 verlangt für diesen Fall FileNameOffset = 0.
|
|
14
|
-
*
|
|
23
|
+
* Da wir seit Bug-1-Fix ohnehin nie mit leerem Pattern senden, kann
|
|
24
|
+
* dieser Bug nicht mehr getriggert werden — encodeQueryDirectory-
|
|
25
|
+
* RequestFixed behandelt den Fall trotzdem spec-konform, falls ein
|
|
26
|
+
* Aufrufer den Encoder direkt benutzt oder ein zukünftiger Server
|
|
27
|
+
* noch strenger prüft.
|
|
15
28
|
*
|
|
16
29
|
* Strategie:
|
|
17
30
|
* Statt smb3-client's `client.readdir()` / `readdirAll` verwenden wir
|
|
@@ -19,11 +32,9 @@
|
|
|
19
32
|
* verwendet (Open, Close, Tree-Connect bleiben unverändert) und nur
|
|
20
33
|
* die kaputte QUERY_DIRECTORY-Loop durch eine spec-konforme ersetzt.
|
|
21
34
|
*
|
|
22
|
-
*
|
|
23
|
-
* smb3-client's `exports`-Gate keine Subpath-Imports
|
|
24
|
-
*
|
|
25
|
-
* Idempotent, thread-safe (Node ist single-threaded, aber wir wollen
|
|
26
|
-
* keinen Race auf paralleler Erst-Nutzung).
|
|
35
|
+
* Die internen wire-Module werden via `file://` URL geladen, weil
|
|
36
|
+
* smb3-client's `exports`-Gate keine Subpath-Imports über den
|
|
37
|
+
* Package-Namen zulässt (nicht einmal `smb3-client/package.json`).
|
|
27
38
|
*/
|
|
28
39
|
|
|
29
40
|
const fs = require("fs");
|
|
@@ -130,9 +141,15 @@ function loadInternals() {
|
|
|
130
141
|
|
|
131
142
|
/**
|
|
132
143
|
* Spec-konformer Encoder für SMB2 QUERY_DIRECTORY (MS-SMB2 §2.2.33).
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
144
|
+
*
|
|
145
|
+
* Unterschied zum Original in smb3-client@0.2.0:
|
|
146
|
+
* `FileNameOffset` wird auf 0 gesetzt, wenn `pat.length === 0`
|
|
147
|
+
* (statt hart auf 64+32=96). MS-SMB2 §2.2.33 verlangt bei
|
|
148
|
+
* `FileNameLength=0` genau `FileNameOffset=0`.
|
|
149
|
+
*
|
|
150
|
+
* In der aktuellen Compat-Loop wird immer `pat="*"` gesendet, daher
|
|
151
|
+
* wird dieser Zweig nie erreicht. Er bleibt als defensive Absicherung
|
|
152
|
+
* gegen Aufrufer, die den Encoder direkt mit leerem Pattern nutzen.
|
|
136
153
|
*/
|
|
137
154
|
function encodeQueryDirectoryRequestFixed(I, req) {
|
|
138
155
|
const pat = req.searchPattern
|
|
@@ -154,8 +171,13 @@ function encodeQueryDirectoryRequestFixed(I, req) {
|
|
|
154
171
|
|
|
155
172
|
/**
|
|
156
173
|
* QUERY_DIRECTORY-Loop mit gepatchtem Encoder.
|
|
157
|
-
*
|
|
158
|
-
*
|
|
174
|
+
*
|
|
175
|
+
* Unterschiede zu smb3-client's readdirAll():
|
|
176
|
+
* 1. Auf Folge-Pages wird `searchPattern="*"` gesendet (nicht "").
|
|
177
|
+
* Samba lehnt leere Patterns strikt ab (STATUS_OBJECT_NAME_INVALID).
|
|
178
|
+
* 2. RESTART_SCANS nur beim ersten Request.
|
|
179
|
+
* 3. Encoder setzt FileNameOffset=0 bei leerem Pattern (Fallback für
|
|
180
|
+
* andere kaputte Server, wird hier faktisch nie benutzt).
|
|
159
181
|
*/
|
|
160
182
|
async function readdirAllFixed(I, open) {
|
|
161
183
|
const items = [];
|
|
@@ -167,7 +189,7 @@ async function readdirAllFixed(I, open) {
|
|
|
167
189
|
flags: first ? I.QueryDirectoryFlag.RESTART_SCANS : 0,
|
|
168
190
|
fileIndex: 0,
|
|
169
191
|
fileId: open.fileId,
|
|
170
|
-
searchPattern:
|
|
192
|
+
searchPattern: "*", // IMMER "*", nie "" — Samba lehnt leer ab
|
|
171
193
|
outputBufferLength: 65536,
|
|
172
194
|
});
|
|
173
195
|
first = false;
|
package/smb-client.js
CHANGED
|
@@ -227,8 +227,8 @@ async function buildClient(config) {
|
|
|
227
227
|
* Convert a readdirCompat rich-Dirent (which already carries name,
|
|
228
228
|
* size, mtime, ctime, and isDirectory()) directly to the legacy
|
|
229
229
|
* `@marsaud/smb2`-compatible shape the rest of the plugin expects.
|
|
230
|
-
* No extra stat roundtrip needed —
|
|
231
|
-
*
|
|
230
|
+
* No extra stat roundtrip needed — attributes come straight from the
|
|
231
|
+
* single QUERY_DIRECTORY response.
|
|
232
232
|
*/
|
|
233
233
|
function mapDirent(dirent) {
|
|
234
234
|
return {
|
|
@@ -255,42 +255,33 @@ async function buildClient(config) {
|
|
|
255
255
|
*/
|
|
256
256
|
async readdir(rel) {
|
|
257
257
|
const full = resolvePath(rel);
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// implementiert QUERY_DIRECTORY MS-SMB2-§2.2.33-
|
|
263
|
-
// umgeht
|
|
264
|
-
// client.readdir()
|
|
258
|
+
// readdirCompat ist der ALLEINIGE readdir-Pfad. Grund: smb3-
|
|
259
|
+
// client@0.2.0 hat zwei QUERY_DIRECTORY-Wire-Bugs, die gegen
|
|
260
|
+
// Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033) führen
|
|
261
|
+
// (leeres Pattern auf Folge-Pages + falscher FileNameOffset).
|
|
262
|
+
// readdirCompat implementiert QUERY_DIRECTORY MS-SMB2-§2.2.33-
|
|
263
|
+
// konform und umgeht beide Bugs. Kein Fallback auf
|
|
264
|
+
// client.readdir() — das würde die Bugs reproduzieren.
|
|
265
265
|
let dirents;
|
|
266
266
|
try {
|
|
267
267
|
dirents = await readdirCompat(client, full, { rich: true });
|
|
268
268
|
} catch (errCompat) {
|
|
269
269
|
const err = errCompat;
|
|
270
|
-
// Some Samba builds (observed on 4.20+/4.23+) reject
|
|
271
|
-
// SMB2 QUERY_DIRECTORY on the *share root* with
|
|
272
|
-
// STATUS_OBJECT_NAME_INVALID (0xC0000033) because smb3-client
|
|
273
|
-
// asks for FileIdBothDirectoryInformation (class 37) on a
|
|
274
|
-
// handle opened with an empty filename. We cannot swap the
|
|
275
|
-
// information class from userland, and Samba refuses both
|
|
276
|
-
// "." (→ STATUS_OBJECT_NAME_NOT_FOUND) and "*" (→ CREATE with
|
|
277
|
-
// wildcard is protocol-illegal). The clean way out is:
|
|
278
|
-
// require the caller to configure a real base_path so every
|
|
279
|
-
// readdir happens inside a directory the server can enumerate.
|
|
280
270
|
const msg = String((err && err.message) || err || "");
|
|
281
271
|
// Falls der Compat-Pfad selbst mit 0xC0000033 zurückkommt,
|
|
282
272
|
// liegt entweder ein weiterer, unbekannter Wire-Bug vor oder
|
|
283
|
-
// Samba lehnt aus einem anderen Grund ab.
|
|
284
|
-
//
|
|
273
|
+
// Samba lehnt aus einem anderen Grund ab (z. B. fehlende
|
|
274
|
+
// Rechte auf die Zielressource). Wir geben eine gezielte
|
|
275
|
+
// Meldung mit Handlungsanweisung.
|
|
285
276
|
if (/0xC0000033|OBJECT_NAME_INVALID/i.test(msg)) {
|
|
286
277
|
const e = new Error(
|
|
287
|
-
"Samba lehnt QUERY_DIRECTORY auf diesem Pfad ab " +
|
|
288
|
-
"(STATUS_OBJECT_NAME_INVALID, 0xC0000033). " +
|
|
289
|
-
"
|
|
290
|
-
"falls dieser Fehler
|
|
291
|
-
"
|
|
292
|
-
"tools/diag-
|
|
293
|
-
"Details: " +
|
|
278
|
+
"Samba lehnt QUERY_DIRECTORY auf diesem Pfad unerwartet ab " +
|
|
279
|
+
"(STATUS_OBJECT_NAME_INVALID, 0xC0000033). Die bekannten " +
|
|
280
|
+
"smb3-client-Wire-Bugs sind in dieser Plugin-Version " +
|
|
281
|
+
"bereits gepatcht — falls dieser Fehler dennoch auftritt, " +
|
|
282
|
+
"liegt ein bislang unbekannter Wire-Format-Fehler oder ein " +
|
|
283
|
+
"Rechteproblem vor. Bitte tools/diag-wire.js ausführen " +
|
|
284
|
+
"und den Report zurücksenden. Details: " +
|
|
294
285
|
msg
|
|
295
286
|
);
|
|
296
287
|
e.cause = err;
|
|
@@ -320,9 +311,9 @@ async function buildClient(config) {
|
|
|
320
311
|
}
|
|
321
312
|
throw err;
|
|
322
313
|
}
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
314
|
+
// readdirCompat gibt Rich-Dirents zurück (Name, Größe, mtime,
|
|
315
|
+
// ctime, isDirectory). Kein zusätzlicher stat-Fan-out nötig —
|
|
316
|
+
// ein einziger QUERY_DIRECTORY-Roundtrip liefert alles.
|
|
326
317
|
return dirents.map(mapDirent);
|
|
327
318
|
},
|
|
328
319
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone diagnostic script for saltcorn-samba.
|
|
4
|
+
* Runs a series of individual SMB3 operations against a share so we can see
|
|
5
|
+
* exactly which primitive fails and why. Prints raw NT status codes.
|
|
6
|
+
*
|
|
7
|
+
* Usage (from the plugin directory, so smb3-client resolves):
|
|
8
|
+
*
|
|
9
|
+
* node tools/diag-basepath.js \
|
|
10
|
+
* --host 192.168.110.10 --share buero --user 01_vassen \
|
|
11
|
+
* --domain buero.ib-vassen.de --password '…' \
|
|
12
|
+
* --path static
|
|
13
|
+
*
|
|
14
|
+
* The script never writes; it only opens handles.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const args = parseArgs(process.argv.slice(2));
|
|
21
|
+
const required = ["host", "share", "user", "path"];
|
|
22
|
+
for (const k of required) {
|
|
23
|
+
if (!args[k]) {
|
|
24
|
+
console.error("missing required --" + k);
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const password = args.password || process.env.SMB_PASSWORD || "";
|
|
29
|
+
if (!password) {
|
|
30
|
+
console.error("missing --password (or SMB_PASSWORD env var)");
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { Client } = await import("smb3-client");
|
|
35
|
+
|
|
36
|
+
const client = new Client({
|
|
37
|
+
host: args.host,
|
|
38
|
+
port: Number(args.port) || 445,
|
|
39
|
+
domain: args.domain || "",
|
|
40
|
+
username: args.user,
|
|
41
|
+
password,
|
|
42
|
+
connectTimeout: 10_000,
|
|
43
|
+
requestTimeout: 30_000,
|
|
44
|
+
signing: args.signing || "if-offered",
|
|
45
|
+
encryption: args.encryption || "if-offered",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log("[1/7] Connecting …");
|
|
49
|
+
await client.connect();
|
|
50
|
+
console.log(" \u2713 TCP + Negotiate + Session-Setup + Auth OK");
|
|
51
|
+
|
|
52
|
+
const share = args.share;
|
|
53
|
+
const path = args.path;
|
|
54
|
+
const shareAndPath = share + "/" + path;
|
|
55
|
+
console.log("[2/7] TREE_CONNECT to \\\\" + args.host + "\\" + share + " \u2026");
|
|
56
|
+
try {
|
|
57
|
+
// Trigger TREE_CONNECT by touching the raw internal — but smb3-client
|
|
58
|
+
// does that implicitly on the first per-share call. So instead we call
|
|
59
|
+
// a cheap op below and let the connect surface the error there.
|
|
60
|
+
console.log(" (deferred until first op)");
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(" \u2717 TREE_CONNECT failed:", e && e.message);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Probes ------------------------------------------------------------
|
|
66
|
+
const probes = [
|
|
67
|
+
{ label: "readdir(share) \u2014 SHARE ROOT", op: () => client.readdir(share) },
|
|
68
|
+
{ label: "readdir(share/'') \u2014 empty subpath", op: () => client.readdir(share + "/") },
|
|
69
|
+
{ label: "stat(share/path) \u2014 target as-is", op: () => client.stat(shareAndPath) },
|
|
70
|
+
{ label: "readdir(share/path) \u2014 target as-is", op: () => client.readdir(shareAndPath) },
|
|
71
|
+
{ label: "readdir(share/PATH) \u2014 target uppercased", op: () => client.readdir(share + "/" + path.toUpperCase()) },
|
|
72
|
+
{ label: "readdir(share/path.lc) \u2014 target lowercased", op: () => client.readdir(share + "/" + path.toLowerCase()) },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
let step = 3;
|
|
76
|
+
for (const p of probes) {
|
|
77
|
+
console.log("[" + step + "/7] " + p.label);
|
|
78
|
+
try {
|
|
79
|
+
const r = await p.op();
|
|
80
|
+
const preview = Array.isArray(r)
|
|
81
|
+
? " (" + r.length + " entries" +
|
|
82
|
+
(r.length ? ", first: " +
|
|
83
|
+
JSON.stringify(r.slice(0, 3).map((e) => (typeof e === "string" ? e : e.name))) : "") + ")"
|
|
84
|
+
: " (" + JSON.stringify(r).slice(0, 120) + ")";
|
|
85
|
+
console.log(" \u2713 OK" + preview);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.log(" \u2717 " + (e && e.message ? e.message.split("\n")[0] : String(e)));
|
|
88
|
+
if (e && e.status) console.log(" NT status: 0x" + e.status.toString(16));
|
|
89
|
+
if (e && e.code) console.log(" error code: " + e.code);
|
|
90
|
+
}
|
|
91
|
+
step++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// smb3-client's Client has close(), not disconnect() — the old
|
|
95
|
+
// marsaud API used disconnect. Use close() and ignore errors so a
|
|
96
|
+
// failing probe still tears down cleanly.
|
|
97
|
+
try { await client.close(); } catch (_) {}
|
|
98
|
+
console.log("Done.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseArgs(argv) {
|
|
102
|
+
const out = {};
|
|
103
|
+
for (let i = 0; i < argv.length; i++) {
|
|
104
|
+
const a = argv[i];
|
|
105
|
+
if (a.startsWith("--")) {
|
|
106
|
+
const key = a.slice(2);
|
|
107
|
+
const nxt = argv[i + 1];
|
|
108
|
+
if (!nxt || nxt.startsWith("--")) { out[key] = true; }
|
|
109
|
+
else { out[key] = nxt; i++; }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch((e) => {
|
|
116
|
+
console.error("FATAL:", e && e.stack || e);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* diag-wire.js — Wire-level QUERY_DIRECTORY diagnostic.
|
|
4
|
+
*
|
|
5
|
+
* Opens a connection, tree-connects, opens the target directory, then sends
|
|
6
|
+
* a hand-built QUERY_DIRECTORY packet with FULL HEX DUMP of both request
|
|
7
|
+
* and response. This bypasses smb3-client's encoders so we can compare bytes.
|
|
8
|
+
*
|
|
9
|
+
* Usage (from the plugin directory):
|
|
10
|
+
* node tools/diag-wire.js \
|
|
11
|
+
* --host 192.168.110.10 --share buero --user 01_vassen \
|
|
12
|
+
* --domain buero.ib-vassen.de --password '...' \
|
|
13
|
+
* --path static
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const url = require("url");
|
|
21
|
+
|
|
22
|
+
async function loadInternals() {
|
|
23
|
+
const candidates = [
|
|
24
|
+
path.join(__dirname, "..", "node_modules", "smb3-client"),
|
|
25
|
+
path.join(__dirname, "..", "..", "node_modules", "smb3-client"),
|
|
26
|
+
];
|
|
27
|
+
let smb3Root = null;
|
|
28
|
+
for (const c of candidates) {
|
|
29
|
+
try {
|
|
30
|
+
fs.accessSync(path.join(c, "dist", "index.js"));
|
|
31
|
+
smb3Root = c;
|
|
32
|
+
break;
|
|
33
|
+
} catch (_) {}
|
|
34
|
+
}
|
|
35
|
+
if (!smb3Root) {
|
|
36
|
+
throw new Error("smb3-client not found in node_modules");
|
|
37
|
+
}
|
|
38
|
+
const distDir = path.join(smb3Root, "dist");
|
|
39
|
+
const load = (rel) => import(url.pathToFileURL(path.join(distDir, rel)).href);
|
|
40
|
+
|
|
41
|
+
const [
|
|
42
|
+
idx,
|
|
43
|
+
bufMod,
|
|
44
|
+
qdMod,
|
|
45
|
+
cmdMod,
|
|
46
|
+
qiMod,
|
|
47
|
+
createStructMod,
|
|
48
|
+
pathsMod,
|
|
49
|
+
errMod,
|
|
50
|
+
openMod,
|
|
51
|
+
] = await Promise.all([
|
|
52
|
+
load("index.js"),
|
|
53
|
+
load("wire/buffer.js"),
|
|
54
|
+
load("wire/structs/queryDirectory.js"),
|
|
55
|
+
load("wire/commands.js"),
|
|
56
|
+
load("wire/structs/queryInfo.js"),
|
|
57
|
+
load("wire/structs/create.js"),
|
|
58
|
+
load("paths.js"),
|
|
59
|
+
load("errors.js"),
|
|
60
|
+
load("open/open.js"),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
Client: idx.Client,
|
|
65
|
+
Writer: bufMod.Writer,
|
|
66
|
+
encodeQueryDirectoryRequest: qdMod.encodeQueryDirectoryRequest,
|
|
67
|
+
decodeQueryDirectoryResponse: qdMod.decodeQueryDirectoryResponse,
|
|
68
|
+
parseFileIdBothDirectoryInformation: qdMod.parseFileIdBothDirectoryInformation,
|
|
69
|
+
QueryDirectoryFlag: qdMod.QueryDirectoryFlag,
|
|
70
|
+
SmbCommand: cmdMod.SmbCommand,
|
|
71
|
+
NTStatus: cmdMod.NTStatus,
|
|
72
|
+
isSuccess: cmdMod.isSuccess,
|
|
73
|
+
statusName: cmdMod.statusName,
|
|
74
|
+
FileInformationClass: qiMod.FileInformationClass,
|
|
75
|
+
FileAccess: createStructMod.FileAccess,
|
|
76
|
+
ShareAccess: createStructMod.ShareAccess,
|
|
77
|
+
CreateDisposition: createStructMod.CreateDisposition,
|
|
78
|
+
CreateOptions: createStructMod.CreateOptions,
|
|
79
|
+
splitSharePath: pathsMod.splitSharePath,
|
|
80
|
+
toSmbPath: pathsMod.toSmbPath,
|
|
81
|
+
SmbError: errMod.SmbError,
|
|
82
|
+
Open: openMod.Open,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hex(buf, label) {
|
|
87
|
+
console.log(`\n--- ${label} (${buf.length} bytes) ---`);
|
|
88
|
+
for (let off = 0; off < buf.length; off += 16) {
|
|
89
|
+
const slice = buf.slice(off, Math.min(off + 16, buf.length));
|
|
90
|
+
const hexPart = Array.from(slice)
|
|
91
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
92
|
+
.join(" ")
|
|
93
|
+
.padEnd(48);
|
|
94
|
+
const asciiPart = Array.from(slice)
|
|
95
|
+
.map((b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : "."))
|
|
96
|
+
.join("");
|
|
97
|
+
console.log(
|
|
98
|
+
`${off.toString(16).padStart(4, "0")} ${hexPart} ${asciiPart}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build QUERY_DIRECTORY body byte-by-byte with named fields
|
|
104
|
+
function buildQdBody(I, opts) {
|
|
105
|
+
const {
|
|
106
|
+
fileInformationClass,
|
|
107
|
+
flags,
|
|
108
|
+
fileIndex,
|
|
109
|
+
fileId,
|
|
110
|
+
searchPattern,
|
|
111
|
+
outputBufferLength,
|
|
112
|
+
forceFileNameOffsetZero, // testing variant
|
|
113
|
+
} = opts;
|
|
114
|
+
|
|
115
|
+
const pat = Buffer.from(searchPattern, "utf16le");
|
|
116
|
+
const w = new I.Writer();
|
|
117
|
+
w.u16(33); // StructureSize
|
|
118
|
+
w.u8(fileInformationClass);
|
|
119
|
+
w.u8(flags);
|
|
120
|
+
w.u32(fileIndex);
|
|
121
|
+
w.bytes(fileId); // 16
|
|
122
|
+
const fileNameOffset = forceFileNameOffsetZero
|
|
123
|
+
? 0
|
|
124
|
+
: pat.length === 0
|
|
125
|
+
? 0
|
|
126
|
+
: 64 + 32;
|
|
127
|
+
w.u16(fileNameOffset);
|
|
128
|
+
w.u16(pat.length);
|
|
129
|
+
w.u32(outputBufferLength);
|
|
130
|
+
if (pat.length === 0) {
|
|
131
|
+
w.u8(0); // pad byte
|
|
132
|
+
} else {
|
|
133
|
+
w.bytes(pat);
|
|
134
|
+
}
|
|
135
|
+
return w.buffer();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function sendRaw(open, I, opts, label) {
|
|
139
|
+
const body = buildQdBody(I, opts);
|
|
140
|
+
hex(body, `${label} REQUEST BODY`);
|
|
141
|
+
console.log(
|
|
142
|
+
`Fields: StructSize=33, FileInformationClass=${opts.fileInformationClass}, ` +
|
|
143
|
+
`Flags=${opts.flags}, FileNameLength=${
|
|
144
|
+
Buffer.from(opts.searchPattern, "utf16le").length
|
|
145
|
+
}, ` +
|
|
146
|
+
`OutputBufferLength=${opts.outputBufferLength}`
|
|
147
|
+
);
|
|
148
|
+
const fnOff = body.readUInt16LE(32 - 8);
|
|
149
|
+
console.log(`FileNameOffset (from body): ${fnOff}`);
|
|
150
|
+
|
|
151
|
+
const signing = open.tree.session.makeSigning();
|
|
152
|
+
try {
|
|
153
|
+
const resp = await open.tree.conn.send(I.SmbCommand.QUERY_DIRECTORY, body, {
|
|
154
|
+
sessionId: open.tree.session.sessionId,
|
|
155
|
+
treeId: open.tree.treeId,
|
|
156
|
+
...(signing !== undefined ? { signing } : {}),
|
|
157
|
+
encrypt: open.tree.encryptRequired,
|
|
158
|
+
creditCharge: 1,
|
|
159
|
+
});
|
|
160
|
+
console.log(
|
|
161
|
+
`RESPONSE status: 0x${resp.header.status.toString(16)} (${I.statusName(
|
|
162
|
+
resp.header.status
|
|
163
|
+
)})`
|
|
164
|
+
);
|
|
165
|
+
hex(resp.body.slice(0, Math.min(resp.body.length, 128)), `${label} RESPONSE BODY (first 128)`);
|
|
166
|
+
if (I.isSuccess(resp.header.status)) {
|
|
167
|
+
const buf = I.decodeQueryDirectoryResponse(resp.body, 64);
|
|
168
|
+
const items = I.parseFileIdBothDirectoryInformation(buf);
|
|
169
|
+
console.log(
|
|
170
|
+
`Decoded ${items.length} entries: ${items.slice(0, 5).map((e) => e.fileName).join(", ")}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return resp;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.log(`SEND ERROR: ${e.message}`);
|
|
176
|
+
if (e.status) console.log(` NT status: 0x${e.status.toString(16)}`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
const args = parseArgs(process.argv.slice(2));
|
|
183
|
+
const required = ["host", "share", "user", "path"];
|
|
184
|
+
for (const k of required) {
|
|
185
|
+
if (!args[k]) {
|
|
186
|
+
console.error(`missing required --${k}`);
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const password = args.password || process.env.SMB_PASSWORD || "";
|
|
191
|
+
if (!password) {
|
|
192
|
+
console.error("missing --password");
|
|
193
|
+
process.exit(2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const I = await loadInternals();
|
|
197
|
+
|
|
198
|
+
const client = new I.Client({
|
|
199
|
+
host: args.host,
|
|
200
|
+
port: Number(args.port) || 445,
|
|
201
|
+
domain: args.domain || "",
|
|
202
|
+
username: args.user,
|
|
203
|
+
password,
|
|
204
|
+
connectTimeout: 10000,
|
|
205
|
+
requestTimeout: 30000,
|
|
206
|
+
signing: args.signing || "if-offered",
|
|
207
|
+
encryption: args.encryption || "if-offered",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
console.log("Connecting…");
|
|
211
|
+
await client.connect();
|
|
212
|
+
console.log("Connected");
|
|
213
|
+
|
|
214
|
+
const fullPath = `${args.share}/${args.path}`;
|
|
215
|
+
const { share, rest } = I.splitSharePath(fullPath);
|
|
216
|
+
const tree = await client.treeFor(share);
|
|
217
|
+
console.log(`Tree connected: treeId=${tree.treeId}`);
|
|
218
|
+
|
|
219
|
+
// Open the target directory
|
|
220
|
+
const open = await I.Open.create(tree, {
|
|
221
|
+
filename: I.toSmbPath(rest),
|
|
222
|
+
desiredAccess:
|
|
223
|
+
I.FileAccess.FILE_READ_DATA | I.FileAccess.FILE_READ_ATTRIBUTES,
|
|
224
|
+
shareAccess: I.ShareAccess.READ | I.ShareAccess.WRITE | I.ShareAccess.DELETE,
|
|
225
|
+
createDisposition: I.CreateDisposition.OPEN,
|
|
226
|
+
createOptions: I.CreateOptions.DIRECTORY_FILE,
|
|
227
|
+
fileAttributes: 0,
|
|
228
|
+
});
|
|
229
|
+
console.log(`Opened dir, fileId hex: ${open.fileId.toString("hex")}`);
|
|
230
|
+
|
|
231
|
+
// ---------------- 6 probes ----------------
|
|
232
|
+
console.log("\n============ PROBE 1: pat='*', flags=RESTART, class=FileIdBothDir ============");
|
|
233
|
+
await sendRaw(
|
|
234
|
+
open,
|
|
235
|
+
I,
|
|
236
|
+
{
|
|
237
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
238
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
239
|
+
fileIndex: 0,
|
|
240
|
+
fileId: open.fileId,
|
|
241
|
+
searchPattern: "*",
|
|
242
|
+
outputBufferLength: 65536,
|
|
243
|
+
},
|
|
244
|
+
"P1"
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
console.log("\n============ PROBE 2: pat='*', flags=0, class=FileIdBothDir ============");
|
|
248
|
+
await sendRaw(
|
|
249
|
+
open,
|
|
250
|
+
I,
|
|
251
|
+
{
|
|
252
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
253
|
+
flags: 0,
|
|
254
|
+
fileIndex: 0,
|
|
255
|
+
fileId: open.fileId,
|
|
256
|
+
searchPattern: "*",
|
|
257
|
+
outputBufferLength: 65536,
|
|
258
|
+
},
|
|
259
|
+
"P2"
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
console.log("\n============ PROBE 3: pat='*', flags=RESTART, class=FileBothDir (0x03) ============");
|
|
263
|
+
await sendRaw(
|
|
264
|
+
open,
|
|
265
|
+
I,
|
|
266
|
+
{
|
|
267
|
+
fileInformationClass: I.FileInformationClass.FileBothDirectoryInformation ?? 3,
|
|
268
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
269
|
+
fileIndex: 0,
|
|
270
|
+
fileId: open.fileId,
|
|
271
|
+
searchPattern: "*",
|
|
272
|
+
outputBufferLength: 65536,
|
|
273
|
+
},
|
|
274
|
+
"P3"
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
console.log("\n============ PROBE 4: pat='*', flags=RESTART, class=FileDirInfo (0x01) ============");
|
|
278
|
+
await sendRaw(
|
|
279
|
+
open,
|
|
280
|
+
I,
|
|
281
|
+
{
|
|
282
|
+
fileInformationClass: I.FileInformationClass.FileDirectoryInformation ?? 1,
|
|
283
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
284
|
+
fileIndex: 0,
|
|
285
|
+
fileId: open.fileId,
|
|
286
|
+
searchPattern: "*",
|
|
287
|
+
outputBufferLength: 65536,
|
|
288
|
+
},
|
|
289
|
+
"P4"
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
console.log("\n============ PROBE 5: pat='*', outBuf=8192, flags=RESTART ============");
|
|
293
|
+
await sendRaw(
|
|
294
|
+
open,
|
|
295
|
+
I,
|
|
296
|
+
{
|
|
297
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
298
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
299
|
+
fileIndex: 0,
|
|
300
|
+
fileId: open.fileId,
|
|
301
|
+
searchPattern: "*",
|
|
302
|
+
outputBufferLength: 8192,
|
|
303
|
+
},
|
|
304
|
+
"P5"
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
console.log("\n============ PROBE 6: pat='', flags=RESTART (empty pattern, offset=0) ============");
|
|
308
|
+
await sendRaw(
|
|
309
|
+
open,
|
|
310
|
+
I,
|
|
311
|
+
{
|
|
312
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
313
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
314
|
+
fileIndex: 0,
|
|
315
|
+
fileId: open.fileId,
|
|
316
|
+
searchPattern: "",
|
|
317
|
+
outputBufferLength: 65536,
|
|
318
|
+
},
|
|
319
|
+
"P6"
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
console.log("\nAvailable FileInformationClass values:");
|
|
323
|
+
console.log(JSON.stringify(I.FileInformationClass, null, 2));
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await open.close();
|
|
327
|
+
} catch (_) {}
|
|
328
|
+
try {
|
|
329
|
+
await client.close();
|
|
330
|
+
} catch (_) {}
|
|
331
|
+
console.log("\nDone.");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function parseArgs(argv) {
|
|
335
|
+
const out = {};
|
|
336
|
+
for (let i = 0; i < argv.length; i++) {
|
|
337
|
+
const a = argv[i];
|
|
338
|
+
if (a.startsWith("--")) {
|
|
339
|
+
const key = a.slice(2);
|
|
340
|
+
const nxt = argv[i + 1];
|
|
341
|
+
if (!nxt || nxt.startsWith("--")) {
|
|
342
|
+
out[key] = true;
|
|
343
|
+
} else {
|
|
344
|
+
out[key] = nxt;
|
|
345
|
+
i++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
main().catch((e) => {
|
|
353
|
+
console.error("FATAL:", (e && e.stack) || e);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
});
|