clever-tools 3.5.2 → 3.6.1

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.
@@ -0,0 +1,3709 @@
1
+ // The MIT License (MIT)
2
+ //
3
+ // Copyright (c) 2014-2016 Nick Carneiro
4
+ //
5
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ // of this software and associated documentation files (the "Software"), to deal
7
+ // in the Software without restriction, including without limitation the rights
8
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ // copies of the Software, and to permit persons to whom the Software is
10
+ // furnished to do so, subject to the following conditions:
11
+ //
12
+ // The above copyright notice and this permission notice shall be included in all
13
+ // copies or substantial portions of the Software.
14
+ //
15
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ // SOFTWARE.
22
+
23
+ 'use strict';
24
+
25
+ var Parser = require('tree-sitter');
26
+ var Bash = require('@curlconverter/tree-sitter-bash');
27
+
28
+ class CCError extends Error {
29
+ }
30
+ const UTF8encoder = new TextEncoder();
31
+ // Note: !has() will lead to type errors
32
+ // TODO: replace with Object.hasOwn() once Node 16 is EOL'd on 2023-09-11
33
+ function has(obj, prop) {
34
+ return Object.prototype.hasOwnProperty.call(obj, prop);
35
+ }
36
+ function isInt(s) {
37
+ return /^\s*[+-]?\d+$/.test(s);
38
+ }
39
+
40
+ // Words act like strings. They're lists of characters, except some
41
+ // characters can be shell variables or expressions.
42
+ // They're implemented like this:
43
+ // ["foobar", {type: "variable", value: "baz", text: "$baz"}, "qux"]
44
+ // Except for the empty string [""], there should be no empty strings in the array.
45
+ // TODO: Words should keep a list of operations that happened to them
46
+ // like .replace() so that we can generate code that also does that operation
47
+ // on the contents of the environment variable or the output of the command.
48
+ class Word {
49
+ constructor(tokens) {
50
+ this.valueOf = Word.toString;
51
+ if (typeof tokens === "string") {
52
+ tokens = [tokens];
53
+ }
54
+ if (tokens === undefined || tokens.length === 0) {
55
+ tokens = [""];
56
+ }
57
+ this.tokens = [];
58
+ for (const t of tokens) {
59
+ if (typeof t === "string") {
60
+ if (this.tokens.length > 0 &&
61
+ typeof this.tokens[this.tokens.length - 1] === "string") {
62
+ // If we have 2+ strings in a row, merge them
63
+ this.tokens[this.tokens.length - 1] += t;
64
+ }
65
+ else if (t) {
66
+ // skip empty strings
67
+ this.tokens.push(t);
68
+ }
69
+ }
70
+ else {
71
+ this.tokens.push(t);
72
+ }
73
+ }
74
+ if (this.tokens.length === 0) {
75
+ this.tokens.push("");
76
+ }
77
+ }
78
+ get length() {
79
+ let len = 0;
80
+ for (const t of this.tokens) {
81
+ if (typeof t === "string") {
82
+ len += t.length;
83
+ }
84
+ else {
85
+ len += 1;
86
+ }
87
+ }
88
+ return len;
89
+ }
90
+ *[Symbol.iterator]() {
91
+ for (const t of this.tokens) {
92
+ if (typeof t === "string") {
93
+ for (const c of t) {
94
+ yield c;
95
+ }
96
+ }
97
+ else {
98
+ yield t;
99
+ }
100
+ }
101
+ }
102
+ // TODO: do we need this function?
103
+ get(index) {
104
+ let i = 0;
105
+ for (const t of this.tokens) {
106
+ if (typeof t === "string") {
107
+ if (i + t.length > index) {
108
+ return t[index - i];
109
+ }
110
+ i += t.length;
111
+ }
112
+ else {
113
+ if (i === index) {
114
+ return t;
115
+ }
116
+ i += 1;
117
+ }
118
+ }
119
+ throw new CCError("Index out of bounds");
120
+ }
121
+ charAt(index = 0) {
122
+ try {
123
+ return this.get(index);
124
+ }
125
+ catch (_a) { }
126
+ return "";
127
+ }
128
+ indexOf(search, start) {
129
+ if (start === undefined) {
130
+ start = 0;
131
+ }
132
+ let i = 0;
133
+ for (const t of this.tokens) {
134
+ if (typeof t === "string") {
135
+ if (i + t.length > start) {
136
+ const index = t.indexOf(search, start - i);
137
+ if (index !== -1) {
138
+ return i + index;
139
+ }
140
+ }
141
+ i += t.length;
142
+ }
143
+ else {
144
+ i += 1;
145
+ }
146
+ }
147
+ return -1;
148
+ }
149
+ // Like indexOf() but accepts a string of characters and returns the index of the first one
150
+ // it finds
151
+ indexOfFirstChar(search) {
152
+ let i = 0;
153
+ for (const t of this.tokens) {
154
+ if (typeof t === "string") {
155
+ for (const c of t) {
156
+ if (search.includes(c)) {
157
+ return i;
158
+ }
159
+ i += 1;
160
+ }
161
+ }
162
+ else {
163
+ i += 1;
164
+ }
165
+ }
166
+ return -1;
167
+ }
168
+ removeFirstChar(c) {
169
+ if (this.length === 0) {
170
+ return new Word();
171
+ }
172
+ if (this.charAt(0) === c) {
173
+ return this.slice(1);
174
+ }
175
+ return this.copy();
176
+ }
177
+ copy() {
178
+ return new Word(this.tokens);
179
+ }
180
+ slice(indexStart, indexEnd) {
181
+ if (indexStart === undefined) {
182
+ indexStart = this.length;
183
+ }
184
+ if (indexEnd === undefined) {
185
+ indexEnd = this.length;
186
+ }
187
+ if (indexStart >= this.length) {
188
+ return new Word();
189
+ }
190
+ if (indexStart < 0) {
191
+ indexStart = Math.max(indexStart + this.length, 0);
192
+ }
193
+ if (indexEnd < 0) {
194
+ indexEnd = Math.max(indexEnd + this.length, 0);
195
+ }
196
+ if (indexEnd <= indexStart) {
197
+ return new Word();
198
+ }
199
+ const ret = [];
200
+ let i = 0;
201
+ for (const t of this.tokens) {
202
+ if (typeof t === "string") {
203
+ if (i + t.length > indexStart) {
204
+ if (i < indexEnd) {
205
+ ret.push(t.slice(Math.max(indexStart - i, 0), indexEnd - i));
206
+ }
207
+ }
208
+ i += t.length;
209
+ }
210
+ else {
211
+ if (i >= indexStart && i < indexEnd) {
212
+ ret.push(t);
213
+ }
214
+ i += 1;
215
+ }
216
+ }
217
+ return new Word(ret);
218
+ }
219
+ // TODO: check
220
+ includes(search, start) {
221
+ if (start === undefined) {
222
+ start = 0;
223
+ }
224
+ let i = 0;
225
+ for (const t of this.tokens) {
226
+ if (typeof t === "string") {
227
+ if (i + t.length > start) {
228
+ if (t.includes(search, start - i)) {
229
+ return true;
230
+ }
231
+ }
232
+ i += t.length;
233
+ }
234
+ else {
235
+ i += 1;
236
+ }
237
+ }
238
+ return false;
239
+ }
240
+ test(search) {
241
+ for (const t of this.tokens) {
242
+ if (typeof t === "string") {
243
+ if (search.test(t)) {
244
+ return true;
245
+ }
246
+ }
247
+ }
248
+ return false;
249
+ }
250
+ prepend(c) {
251
+ const ret = this.copy();
252
+ if (ret.tokens.length && typeof ret.tokens[0] === "string") {
253
+ ret.tokens[0] = c + ret.tokens[0];
254
+ }
255
+ else {
256
+ ret.tokens.unshift(c);
257
+ }
258
+ return ret;
259
+ }
260
+ append(c) {
261
+ const ret = this.copy();
262
+ if (ret.tokens.length &&
263
+ typeof ret.tokens[ret.tokens.length - 1] === "string") {
264
+ ret.tokens[ret.tokens.length - 1] += c;
265
+ }
266
+ else {
267
+ ret.tokens.push(c);
268
+ }
269
+ return ret;
270
+ }
271
+ // Merges two Words
272
+ add(other) {
273
+ return new Word([...this.tokens, ...other.tokens]);
274
+ }
275
+ // Returns the first match, searches each string independently
276
+ // TODO: improve this
277
+ match(regex) {
278
+ for (const t of this.tokens) {
279
+ if (typeof t === "string") {
280
+ const match = t.match(regex);
281
+ if (match) {
282
+ return match;
283
+ }
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ search(regex) {
289
+ let offset = 0;
290
+ for (const t of this.tokens) {
291
+ if (typeof t === "string") {
292
+ const match = t.search(regex);
293
+ if (match !== -1) {
294
+ return offset + match;
295
+ }
296
+ offset += t.length;
297
+ }
298
+ }
299
+ return -1;
300
+ }
301
+ // .replace() is called per-string, so it won't work through shell variables
302
+ replace(search, replacement) {
303
+ const ret = [];
304
+ for (const t of this.tokens) {
305
+ if (typeof t === "string") {
306
+ ret.push(t.replace(search, replacement));
307
+ }
308
+ else {
309
+ ret.push(t);
310
+ }
311
+ }
312
+ return new Word(ret);
313
+ }
314
+ // splits correctly, not like String.split()
315
+ // The last entry can contain the separator if limit entries has been reached
316
+ split(separator, limit) {
317
+ const ret = [];
318
+ let i = 0;
319
+ let start = 0;
320
+ while (i < this.length) {
321
+ let match = true;
322
+ for (let j = 0; j < separator.length; j++) {
323
+ if (this.get(i + j) !== separator.charAt(j)) {
324
+ match = false;
325
+ break;
326
+ }
327
+ }
328
+ if (match) {
329
+ ret.push(this.slice(start, i));
330
+ i += separator.length;
331
+ start = i;
332
+ if (limit !== undefined && ret.length === limit - 1) {
333
+ break;
334
+ }
335
+ }
336
+ else {
337
+ i += 1;
338
+ }
339
+ }
340
+ if (start <= this.length) {
341
+ ret.push(this.slice(start));
342
+ }
343
+ return ret;
344
+ }
345
+ toLowerCase() {
346
+ return new Word(this.tokens.map((t) => (typeof t === "string" ? t.toLowerCase() : t)));
347
+ }
348
+ toUpperCase() {
349
+ return new Word(this.tokens.map((t) => (typeof t === "string" ? t.toUpperCase() : t)));
350
+ }
351
+ trimStart() {
352
+ const ret = [];
353
+ let i, t;
354
+ for ([i, t] of this.tokens.entries()) {
355
+ if (typeof t === "string") {
356
+ if (i === 0) {
357
+ t = t.trimStart();
358
+ }
359
+ if (t) {
360
+ ret.push(t);
361
+ }
362
+ }
363
+ else {
364
+ ret.push(t);
365
+ }
366
+ }
367
+ if (ret.length === 0) {
368
+ return new Word();
369
+ }
370
+ return new Word(ret);
371
+ }
372
+ trimEnd() {
373
+ const ret = [];
374
+ let i, t;
375
+ for ([i, t] of this.tokens.entries()) {
376
+ if (typeof t === "string") {
377
+ if (i === this.tokens.length - 1) {
378
+ t = t.trimEnd();
379
+ }
380
+ if (t) {
381
+ ret.push(t);
382
+ }
383
+ }
384
+ else {
385
+ ret.push(t);
386
+ }
387
+ }
388
+ if (ret.length === 0) {
389
+ return new Word();
390
+ }
391
+ return new Word(ret);
392
+ }
393
+ trim() {
394
+ const ret = [];
395
+ let i, t;
396
+ for ([i, t] of this.tokens.entries()) {
397
+ if (typeof t === "string") {
398
+ if (i === 0) {
399
+ t = t.trimStart();
400
+ }
401
+ if (i === this.tokens.length - 1) {
402
+ t = t.trimEnd();
403
+ }
404
+ if (t) {
405
+ ret.push(t);
406
+ }
407
+ }
408
+ else {
409
+ ret.push(t);
410
+ }
411
+ }
412
+ if (ret.length === 0) {
413
+ return new Word();
414
+ }
415
+ return new Word(ret);
416
+ }
417
+ isEmpty() {
418
+ if (this.tokens.length === 0) {
419
+ return true;
420
+ }
421
+ if (this.tokens.length === 1 && typeof this.tokens[0] === "string") {
422
+ return this.tokens[0].length === 0;
423
+ }
424
+ return false;
425
+ }
426
+ toBool() {
427
+ return !this.isEmpty();
428
+ }
429
+ // Returns true if .tokens contains no variables/commands
430
+ isString() {
431
+ for (const t of this.tokens) {
432
+ if (typeof t !== "string") {
433
+ return false;
434
+ }
435
+ }
436
+ return true;
437
+ }
438
+ firstShellToken() {
439
+ for (const t of this.tokens) {
440
+ if (typeof t !== "string") {
441
+ return t;
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+ startsWith(prefix) {
447
+ if (this.tokens.length === 0) {
448
+ return false;
449
+ }
450
+ if (typeof this.tokens[0] === "string") {
451
+ return this.tokens[0].startsWith(prefix);
452
+ }
453
+ return false;
454
+ }
455
+ endsWith(suffix) {
456
+ if (this.tokens.length === 0) {
457
+ return false;
458
+ }
459
+ const lastToken = this.tokens[this.tokens.length - 1];
460
+ if (typeof lastToken === "string") {
461
+ return lastToken.endsWith(suffix);
462
+ }
463
+ return false;
464
+ }
465
+ // This destroys the information about the original tokenization
466
+ toString() {
467
+ return this.tokens
468
+ .map((t) => (typeof t === "string" ? t : t.text))
469
+ .join("");
470
+ }
471
+ }
472
+ function eq(it, other) {
473
+ if (it === undefined ||
474
+ it === null ||
475
+ other === undefined ||
476
+ other === null) {
477
+ return it === other;
478
+ }
479
+ if (typeof other === "string") {
480
+ return (it.tokens.length === 1 &&
481
+ typeof it.tokens[0] === "string" &&
482
+ it.tokens[0] === other);
483
+ }
484
+ return (it.tokens.length === other.tokens.length &&
485
+ it.tokens.every((itToken, i) => {
486
+ const otherToken = other.tokens[i];
487
+ if (typeof itToken === "string") {
488
+ return itToken === otherToken;
489
+ }
490
+ else if (typeof otherToken !== "string") {
491
+ return itToken.text === otherToken.text;
492
+ }
493
+ return false;
494
+ }));
495
+ }
496
+ function firstShellToken(word) {
497
+ if (typeof word === "string") {
498
+ return null;
499
+ }
500
+ return word.firstShellToken();
501
+ }
502
+ function mergeWords(...words) {
503
+ const ret = [];
504
+ for (const w of words) {
505
+ if (w instanceof Word) {
506
+ ret.push(...w.tokens);
507
+ }
508
+ else {
509
+ ret.push(w);
510
+ }
511
+ }
512
+ return new Word(ret);
513
+ }
514
+ function joinWords(words, joinChar) {
515
+ const ret = [];
516
+ for (const w of words) {
517
+ if (ret.length) {
518
+ ret.push(joinChar);
519
+ }
520
+ ret.push(...w.tokens);
521
+ }
522
+ return new Word(ret);
523
+ }
524
+
525
+ const parser = new Parser();
526
+ parser.setLanguage(Bash);
527
+
528
+ function warnf(global, warning) {
529
+ global.warnings.push(warning);
530
+ }
531
+ function underlineNode(node, curlCommand) {
532
+ // doesn't include leading whitespace
533
+ const command = node.tree.rootNode;
534
+ let startIndex = node.startIndex;
535
+ let endIndex = node.endIndex;
536
+ if (!curlCommand) {
537
+ curlCommand = command.text;
538
+ startIndex -= command.startIndex;
539
+ endIndex -= command.startIndex;
540
+ }
541
+ if (startIndex === endIndex) {
542
+ endIndex++;
543
+ }
544
+ // TODO: \r ?
545
+ let lineStart = startIndex;
546
+ if (startIndex > 0) {
547
+ // If it's -1 we're on the first line
548
+ lineStart = curlCommand.lastIndexOf("\n", startIndex - 1) + 1;
549
+ }
550
+ let underlineLength = endIndex - startIndex;
551
+ let lineEnd = curlCommand.indexOf("\n", startIndex);
552
+ if (lineEnd === -1) {
553
+ lineEnd = curlCommand.length;
554
+ }
555
+ else if (lineEnd < endIndex) {
556
+ // Add extra "^" past the end of a line to signal that the node continues
557
+ underlineLength = lineEnd - startIndex + 1;
558
+ }
559
+ const line = curlCommand.slice(lineStart, lineEnd);
560
+ const underline = " ".repeat(startIndex - lineStart) + "^".repeat(underlineLength);
561
+ return line + "\n" + underline;
562
+ }
563
+ function warnIfPartsIgnored(request, warnings, support) {
564
+ if (request.urls.length > 1 && !(support === null || support === void 0 ? void 0 : support.multipleUrls)) {
565
+ warnings.push([
566
+ "multiple-urls",
567
+ "found " +
568
+ request.urls.length +
569
+ " URLs, only the first one will be used: " +
570
+ request.urls
571
+ .map((u) => JSON.stringify(u.originalUrl.toString()))
572
+ .join(", "),
573
+ ]);
574
+ }
575
+ if (request.dataReadsFile && !(support === null || support === void 0 ? void 0 : support.dataReadsFile)) {
576
+ warnings.push([
577
+ "unsafe-data",
578
+ // TODO: better wording. Could be "body:" too
579
+ "the generated data content is wrong, " +
580
+ // TODO: might not come from "@"
581
+ JSON.stringify("@" + request.dataReadsFile) +
582
+ " means read the file " +
583
+ JSON.stringify(request.dataReadsFile),
584
+ ]);
585
+ }
586
+ if (request.urls[0].queryReadsFile && !(support === null || support === void 0 ? void 0 : support.queryReadsFile)) {
587
+ warnings.push([
588
+ "unsafe-query",
589
+ "the generated URL query string is wrong, " +
590
+ JSON.stringify("@" + request.urls[0].queryReadsFile) +
591
+ " means read the file " +
592
+ JSON.stringify(request.urls[0].queryReadsFile),
593
+ ]);
594
+ }
595
+ if (request.cookieFiles && !(support === null || support === void 0 ? void 0 : support.cookieFiles)) {
596
+ warnings.push([
597
+ "cookie-files",
598
+ "passing a file for --cookie/-b is not supported: " +
599
+ request.cookieFiles.map((c) => JSON.stringify(c.toString())).join(", "),
600
+ ]);
601
+ }
602
+ }
603
+
604
+ const BACKSLASHES = /\\./gs;
605
+ function removeBackslash(m) {
606
+ return m.charAt(1) === "\n" ? "" : m.charAt(1);
607
+ }
608
+ function removeBackslashes(str) {
609
+ return str.replace(BACKSLASHES, removeBackslash);
610
+ }
611
+ // https://www.gnu.org/software/bash/manual/bash.html#Double-Quotes
612
+ const DOUBLE_QUOTE_BACKSLASHES = /\\[\\$`"\n]/gs;
613
+ function removeDoubleQuoteBackslashes(str) {
614
+ return str.replace(DOUBLE_QUOTE_BACKSLASHES, removeBackslash);
615
+ }
616
+ // ANSI-C quoted strings look $'like this'.
617
+ // Not all shells have them but Bash does
618
+ // https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
619
+ //
620
+ // https://git.savannah.gnu.org/cgit/bash.git/tree/lib/sh/strtrans.c
621
+ const ANSI_BACKSLASHES = /\\(\\|a|b|e|E|f|n|r|t|v|'|"|\?|[0-7]{1,3}|x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|c.)/gs;
622
+ function removeAnsiCBackslashes(str) {
623
+ function unescapeChar(m) {
624
+ switch (m.charAt(1)) {
625
+ case "\\":
626
+ return "\\";
627
+ case "a":
628
+ return "\x07";
629
+ case "b":
630
+ return "\b";
631
+ case "e":
632
+ case "E":
633
+ return "\x1B";
634
+ case "f":
635
+ return "\f";
636
+ case "n":
637
+ return "\n";
638
+ case "r":
639
+ return "\r";
640
+ case "t":
641
+ return "\t";
642
+ case "v":
643
+ return "\v";
644
+ case "'":
645
+ return "'";
646
+ case '"':
647
+ return '"';
648
+ case "?":
649
+ return "?";
650
+ case "c":
651
+ // Bash handles all characters by considering the first byte
652
+ // of its UTF-8 input and can produce invalid UTF-8, whereas
653
+ // JavaScript stores strings in UTF-16
654
+ if (m.codePointAt(2) > 127) {
655
+ throw new CCError('non-ASCII control character in ANSI-C quoted string: "\\u{' +
656
+ m.codePointAt(2).toString(16) +
657
+ '}"');
658
+ }
659
+ // If this produces a 0x00 (null) character, it will cause bash to
660
+ // terminate the string at that character, but we return the null
661
+ // character in the result.
662
+ return m[2] === "?"
663
+ ? "\x7F"
664
+ : String.fromCodePoint(m[2].toUpperCase().codePointAt(0) & 0b00011111);
665
+ case "x":
666
+ case "u":
667
+ case "U":
668
+ // Hexadecimal character literal
669
+ // Unlike bash, this will error if the the code point is greater than 10FFFF
670
+ return String.fromCodePoint(parseInt(m.slice(2), 16));
671
+ case "0":
672
+ case "1":
673
+ case "2":
674
+ case "3":
675
+ case "4":
676
+ case "5":
677
+ case "6":
678
+ case "7":
679
+ // Octal character literal
680
+ return String.fromCodePoint(parseInt(m.slice(1), 8) % 256);
681
+ default:
682
+ // There must be a mis-match between ANSI_BACKSLASHES and the switch statement
683
+ throw new CCError("unhandled character in ANSI-C escape code: " + JSON.stringify(m));
684
+ }
685
+ }
686
+ return str.replace(ANSI_BACKSLASHES, unescapeChar);
687
+ }
688
+ function toTokens(node, curlCommand, warnings) {
689
+ let vals = [];
690
+ switch (node.type) {
691
+ case "word":
692
+ return [removeBackslashes(node.text)];
693
+ case "raw_string":
694
+ return [node.text.slice(1, -1)];
695
+ case "ansi_c_string":
696
+ return [removeAnsiCBackslashes(node.text.slice(2, -1))];
697
+ case "string":
698
+ case "translated_string": {
699
+ // TODO: MISSING quotes, for example
700
+ // curl "example.com
701
+ let prevEnd = node.type === "string" ? 1 : 2;
702
+ let res = "";
703
+ for (const child of node.namedChildren) {
704
+ res += removeDoubleQuoteBackslashes(node.text.slice(prevEnd, child.startIndex - node.startIndex));
705
+ // expansion, simple_expansion or command_substitution (or concat?)
706
+ const subVal = toTokens(child, curlCommand, warnings);
707
+ if (typeof subVal === "string") {
708
+ res += subVal;
709
+ }
710
+ else {
711
+ if (res) {
712
+ vals.push(res);
713
+ res = "";
714
+ }
715
+ vals = vals.concat(subVal);
716
+ }
717
+ prevEnd = child.endIndex - node.startIndex;
718
+ }
719
+ res += removeDoubleQuoteBackslashes(node.text.slice(prevEnd, -1));
720
+ if (res || vals.length === 0) {
721
+ vals.push(res);
722
+ }
723
+ return vals;
724
+ }
725
+ case "simple_expansion":
726
+ // TODO: handle variables downstream
727
+ // '$' + variable_name or special_variable_name
728
+ warnings.push([
729
+ "expansion",
730
+ "found environment variable\n" + underlineNode(node, curlCommand),
731
+ ]);
732
+ if (node.firstNamedChild &&
733
+ node.firstNamedChild.type === "special_variable_name") {
734
+ // https://www.gnu.org/software/bash/manual/bash.html#Special-Parameters
735
+ // TODO: warning isn't printed
736
+ warnings.push([
737
+ "special_variable_name",
738
+ node.text +
739
+ " is a special Bash variable\n" +
740
+ underlineNode(node.firstNamedChild, curlCommand),
741
+ ]);
742
+ }
743
+ return [
744
+ {
745
+ type: "variable",
746
+ value: node.text.slice(1),
747
+ text: node.text,
748
+ syntaxNode: node,
749
+ },
750
+ ];
751
+ case "expansion":
752
+ // Expansions look ${like_this}
753
+ // https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion
754
+ // TODO: MISSING }, for example
755
+ // curl example${com
756
+ warnings.push([
757
+ "expansion",
758
+ "found expansion expression\n" + underlineNode(node, curlCommand),
759
+ ]);
760
+ // variable_name or subscript or no child
761
+ // TODO: handle substitutions
762
+ return [
763
+ {
764
+ type: "variable",
765
+ value: node.text.slice(2, -1),
766
+ text: node.text,
767
+ syntaxNode: node,
768
+ },
769
+ ];
770
+ case "command_substitution":
771
+ // TODO: MISSING ), for example
772
+ // curl example$(com
773
+ warnings.push([
774
+ "expansion",
775
+ "found command substitution expression\n" +
776
+ underlineNode(node, curlCommand),
777
+ ]);
778
+ return [
779
+ {
780
+ type: "command",
781
+ // TODO: further tokenize and pass an array of args
782
+ // to subprocess.run() or a command name + string args to C#
783
+ value: node.text.slice(node.text.startsWith("$(") ? 2 : 1, -1),
784
+ text: node.text,
785
+ syntaxNode: node,
786
+ },
787
+ ];
788
+ case "concatenation": {
789
+ // item[]=1 turns into item=1 if we don't do this
790
+ // https://github.com/tree-sitter/tree-sitter-bash/issues/104
791
+ let prevEnd = 0;
792
+ let res = "";
793
+ for (const child of node.children) {
794
+ // TODO: removeBackslashes()?
795
+ // Can we get anything other than []{} characters here?
796
+ res += node.text.slice(prevEnd, child.startIndex - node.startIndex);
797
+ prevEnd = child.endIndex - node.startIndex;
798
+ const subVal = toTokens(child, curlCommand, warnings);
799
+ if (typeof subVal === "string") {
800
+ res += subVal;
801
+ }
802
+ else {
803
+ if (res) {
804
+ vals.push(res);
805
+ res = "";
806
+ }
807
+ vals = vals.concat(subVal);
808
+ }
809
+ }
810
+ res += node.text.slice(prevEnd);
811
+ if (res || vals.length === 0) {
812
+ vals.push(res);
813
+ }
814
+ return vals;
815
+ }
816
+ default:
817
+ throw new CCError("unexpected argument type " +
818
+ JSON.stringify(node.type) +
819
+ '. Must be one of "word", "string", "raw_string", "ansi_c_string", "expansion", "simple_expansion", "translated_string" or "concatenation"\n' +
820
+ underlineNode(node, curlCommand));
821
+ }
822
+ }
823
+ function toWord(node, curlCommand, warnings) {
824
+ return new Word(toTokens(node, curlCommand, warnings));
825
+ }
826
+ function warnAboutErrorNodes(ast, curlCommand, warnings) {
827
+ // TODO: get only named children?
828
+ const cursor = ast.walk();
829
+ cursor.gotoFirstChild();
830
+ while (cursor.gotoNextSibling()) {
831
+ if (cursor.nodeType === "ERROR") {
832
+ let currentNode = cursor.currentNode;
833
+ try {
834
+ // TreeCursor.currentNode is a property in Node but a function in the browser
835
+ // https://github.com/tree-sitter/tree-sitter/issues/2195
836
+ currentNode = cursor.currentNode();
837
+ }
838
+ catch (_a) { }
839
+ warnings.push([
840
+ "bash",
841
+ `Bash parsing error on line ${cursor.startPosition.row + 1}:\n` +
842
+ underlineNode(currentNode, curlCommand),
843
+ ]);
844
+ break;
845
+ }
846
+ }
847
+ }
848
+ function warnAboutUselessBackslash(n, curlCommandLines, warnings) {
849
+ const lastCommandLine = curlCommandLines[n.endPosition.row];
850
+ const impromperBackslash = lastCommandLine.match(/\\\s+$/);
851
+ if (impromperBackslash &&
852
+ curlCommandLines.length > n.endPosition.row + 1 &&
853
+ impromperBackslash.index !== undefined) {
854
+ warnings.push([
855
+ "unescaped-newline",
856
+ "The trailling '\\' on line " +
857
+ (n.endPosition.row + 1) +
858
+ " is followed by whitespace, so it won't escape the newline after it:\n" +
859
+ // TODO: cut off line if it's very long?
860
+ lastCommandLine +
861
+ "\n" +
862
+ " ".repeat(impromperBackslash.index) +
863
+ "^".repeat(impromperBackslash[0].length),
864
+ ]);
865
+ }
866
+ }
867
+ function extractRedirect(node, curlCommand, warnings) {
868
+ if (!node.childCount) {
869
+ throw new CCError('got empty "redirected_statement" AST node');
870
+ }
871
+ let stdin, stdinFile;
872
+ const [command, ...redirects] = node.namedChildren;
873
+ if (command.type !== "command") {
874
+ throw new CCError('got "redirected_statement" AST node whose first child is not a "command", got ' +
875
+ command.type +
876
+ " instead\n" +
877
+ underlineNode(command, curlCommand));
878
+ }
879
+ if (node.childCount < 2) {
880
+ throw new CCError('got "redirected_statement" AST node with only one child - no redirect');
881
+ }
882
+ if (redirects.length > 1) {
883
+ warnings.push([
884
+ "multiple-redirects",
885
+ // TODO: this is misleading because not all generators use the redirect
886
+ "found " +
887
+ redirects.length +
888
+ " redirect nodes. Only the first one will be used:\n" +
889
+ underlineNode(redirects[1], curlCommand),
890
+ ]);
891
+ }
892
+ const redirect = redirects[0];
893
+ if (redirect.type === "file_redirect") {
894
+ stdinFile = toWord(redirect.namedChildren[0], curlCommand, warnings);
895
+ }
896
+ else if (redirect.type === "heredoc_redirect") {
897
+ // heredoc bodies are children of the parent program node
898
+ // https://github.com/tree-sitter/tree-sitter-bash/issues/118
899
+ if (redirect.namedChildCount < 1) {
900
+ throw new CCError('got "redirected_statement" AST node with heredoc but no heredoc start');
901
+ }
902
+ const heredocStart = redirect.namedChildren[0].text;
903
+ const heredocBody = node.nextNamedSibling;
904
+ if (!heredocBody) {
905
+ throw new CCError('got "redirected_statement" AST node with no heredoc body');
906
+ }
907
+ // TODO: herestrings and heredocs are different
908
+ if (heredocBody.type !== "heredoc_body") {
909
+ throw new CCError('got "redirected_statement" AST node with heredoc but no heredoc body, got ' +
910
+ heredocBody.type +
911
+ " instead");
912
+ }
913
+ // TODO: heredocs can do variable expansion and stuff
914
+ if (heredocStart.length) {
915
+ stdin = new Word(heredocBody.text.slice(0, -heredocStart.length));
916
+ }
917
+ else {
918
+ // this shouldn't happen
919
+ stdin = new Word(heredocBody.text);
920
+ }
921
+ }
922
+ else if (redirect.type === "herestring_redirect") {
923
+ if (redirect.namedChildCount < 1 || !redirect.firstNamedChild) {
924
+ throw new CCError('got "redirected_statement" AST node with empty herestring');
925
+ }
926
+ // TODO: this just converts bash code to text
927
+ stdin = new Word(redirect.firstNamedChild.text);
928
+ }
929
+ else {
930
+ throw new CCError('got "redirected_statement" AST node whose second child is not one of "file_redirect", "heredoc_redirect" or "herestring_redirect", got ' +
931
+ command.type +
932
+ " instead");
933
+ }
934
+ return [command, stdin, stdinFile];
935
+ }
936
+ function _findCurlInPipeline(node, curlCommand, warnings) {
937
+ let command, stdin, stdinFile;
938
+ for (const child of node.namedChildren) {
939
+ if (child.type === "command") {
940
+ const commandName = child.namedChildren[0];
941
+ if (commandName.type !== "command_name") {
942
+ throw new CCError('got "command" AST node whose first child is not a "command_name", got ' +
943
+ commandName.type +
944
+ " instead\n" +
945
+ underlineNode(commandName, curlCommand));
946
+ }
947
+ const commandNameWord = commandName.namedChildren[0];
948
+ if (commandNameWord.type !== "word") {
949
+ throw new CCError('got "command_name" AST node whose first child is not a "word", got ' +
950
+ commandNameWord.type +
951
+ " instead\n" +
952
+ underlineNode(commandNameWord, curlCommand));
953
+ }
954
+ if (commandNameWord.text === "curl") {
955
+ if (!command) {
956
+ command = child;
957
+ }
958
+ else {
959
+ warnings.push([
960
+ "multiple-curl-in-pipeline",
961
+ "found multiple curl commands in pipeline:\n" +
962
+ underlineNode(child, curlCommand),
963
+ ]);
964
+ }
965
+ }
966
+ }
967
+ else if (child.type === "redirected_statement") {
968
+ const [redirCommand, redirStdin, redirStdinFile] = extractRedirect(child, curlCommand, warnings);
969
+ if (redirCommand.namedChildren[0].text === "curl") {
970
+ if (!command) {
971
+ [command, stdin, stdinFile] = [
972
+ redirCommand,
973
+ redirStdin,
974
+ redirStdinFile,
975
+ ];
976
+ }
977
+ else {
978
+ warnings.push([
979
+ "multiple-curl-in-pipeline",
980
+ "found multiple curl commands in pipeline:\n" +
981
+ underlineNode(redirCommand, curlCommand),
982
+ ]);
983
+ }
984
+ }
985
+ }
986
+ else if (child.type === "pipeline") {
987
+ // pipelines can be nested
988
+ // https://github.com/tree-sitter/tree-sitter-bash/issues/167
989
+ const [nestedCommand, nestedStdin, nestedStdinFile] = _findCurlInPipeline(child, curlCommand, warnings);
990
+ if (!nestedCommand) {
991
+ continue;
992
+ }
993
+ if (nestedCommand.namedChildren[0].text === "curl") {
994
+ if (!command) {
995
+ [command, stdin, stdinFile] = [
996
+ nestedCommand,
997
+ nestedStdin,
998
+ nestedStdinFile,
999
+ ];
1000
+ }
1001
+ else {
1002
+ warnings.push([
1003
+ "multiple-curl-in-pipeline",
1004
+ "found multiple curl commands in pipeline:\n" +
1005
+ underlineNode(nestedCommand, curlCommand),
1006
+ ]);
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ return [command, stdin, stdinFile];
1012
+ }
1013
+ // TODO: use pipeline input/output redirects,
1014
+ // i.e. add stdinCommand and stdout/stdoutFile/stdoutCommand
1015
+ function findCurlInPipeline(node, curlCommand, warnings) {
1016
+ const [command, stdin, stdinFile] = _findCurlInPipeline(node, curlCommand, warnings);
1017
+ if (!command) {
1018
+ throw new CCError("could not find curl command in pipeline\n" +
1019
+ underlineNode(node, curlCommand));
1020
+ }
1021
+ return [command, stdin, stdinFile];
1022
+ }
1023
+ // TODO: check entire AST for ERROR/MISSING nodes
1024
+ // TODO: get all command nodes
1025
+ function extractCommandNodes(ast, curlCommand, warnings) {
1026
+ // https://github.com/tree-sitter/tree-sitter-bash/blob/master/grammar.js
1027
+ // The AST must be in a nice format, i.e.
1028
+ // (program
1029
+ // (command
1030
+ // name: (command_name (word))
1031
+ // argument+: (
1032
+ // word |
1033
+ // "string" |
1034
+ // 'raw_string' |
1035
+ // $'ansi_c_string' |
1036
+ // $"translated_string" |
1037
+ // ${expansion} |
1038
+ // $simple_expansion |
1039
+ // concatenation)))
1040
+ // or
1041
+ // (program
1042
+ // (redirected_statement
1043
+ // body: (command, same as above)
1044
+ // redirect))
1045
+ // Shouldn't happen.
1046
+ if (ast.rootNode.type !== "program") {
1047
+ // TODO: better error message.
1048
+ throw new CCError(
1049
+ // TODO: expand "AST" acronym the first time it appears in an error message
1050
+ 'expected a "program" top-level AST node, got ' +
1051
+ ast.rootNode.type +
1052
+ " instead");
1053
+ }
1054
+ if (ast.rootNode.namedChildCount < 1 || !ast.rootNode.namedChildren) {
1055
+ // TODO: better error message.
1056
+ throw new CCError('empty "program" node');
1057
+ }
1058
+ const curlCommandLines = curlCommand.split("\n");
1059
+ let sawComment = false;
1060
+ const commands = [];
1061
+ // Get top-level command and redirected_statement AST nodes, skipping comments
1062
+ for (const n of ast.rootNode.namedChildren) {
1063
+ switch (n.type) {
1064
+ case "comment":
1065
+ sawComment = true;
1066
+ continue;
1067
+ case "command":
1068
+ commands.push([n, undefined, undefined]);
1069
+ warnAboutUselessBackslash(n, curlCommandLines, warnings);
1070
+ break;
1071
+ case "redirected_statement":
1072
+ commands.push(extractRedirect(n, curlCommand, warnings));
1073
+ warnAboutUselessBackslash(n, curlCommandLines, warnings);
1074
+ break;
1075
+ case "pipeline":
1076
+ commands.push(findCurlInPipeline(n, curlCommand, warnings));
1077
+ warnAboutUselessBackslash(n, curlCommandLines, warnings);
1078
+ break;
1079
+ case "heredoc_body": // https://github.com/tree-sitter/tree-sitter-bash/issues/118
1080
+ continue;
1081
+ case "ERROR":
1082
+ throw new CCError(`Bash parsing error on line ${n.startPosition.row + 1}:\n` +
1083
+ underlineNode(n, curlCommand));
1084
+ default:
1085
+ // TODO: better error message.
1086
+ throw new CCError("found " +
1087
+ JSON.stringify(n.type) +
1088
+ ' AST node, only "command", "pipeline" or "redirected_statement" are supported\n' +
1089
+ underlineNode(n, curlCommand));
1090
+ }
1091
+ }
1092
+ if (!commands.length) {
1093
+ // NOTE: if you add more node types in the `for` loop above, this error needs to be updated.
1094
+ // We would probably need to keep track of the node types we've seen.
1095
+ throw new CCError('expected a "command" or "redirected_statement" AST node' +
1096
+ (sawComment ? ', only found "comment" nodes' : ""));
1097
+ }
1098
+ return commands;
1099
+ }
1100
+ function toNameAndArgv(command, curlCommand, warnings) {
1101
+ if (command.childCount < 1) {
1102
+ // TODO: better error message.
1103
+ throw new CCError('empty "command" node\n' + underlineNode(command, curlCommand));
1104
+ }
1105
+ // TODO: add childrenForFieldName to tree-sitter node/web bindings
1106
+ let commandNameLoc = 0;
1107
+ // TODO: parse variable_assignment nodes and replace variables in the command
1108
+ // TODO: support file_redirect
1109
+ for (const n of command.namedChildren) {
1110
+ if (n.type === "variable_assignment" || n.type === "file_redirect") {
1111
+ warnings.push([
1112
+ "command-preamble",
1113
+ "skipping " +
1114
+ JSON.stringify(n.type) +
1115
+ " expression\n" +
1116
+ underlineNode(n, curlCommand),
1117
+ ]);
1118
+ commandNameLoc += 1;
1119
+ }
1120
+ else {
1121
+ // it must be the command name
1122
+ if (n.type !== "command_name") {
1123
+ throw new CCError('expected "command_name", "variable_assignment" or "file_redirect" AST node, found ' +
1124
+ n.type +
1125
+ " instead\n" +
1126
+ underlineNode(n, curlCommand));
1127
+ }
1128
+ break;
1129
+ }
1130
+ }
1131
+ const [name, ...args] = command.namedChildren.slice(commandNameLoc);
1132
+ // Shouldn't happen
1133
+ if (name === undefined) {
1134
+ throw new CCError('found "command" AST node with no "command_name" child\n' +
1135
+ underlineNode(command, curlCommand));
1136
+ }
1137
+ return [name, args];
1138
+ }
1139
+ // Checks that name is "curl"
1140
+ function nameToWord(name, curlCommand, warnings) {
1141
+ if (name.childCount < 1 || !name.firstChild) {
1142
+ throw new CCError('found empty "command_name" AST node\n' + underlineNode(name, curlCommand));
1143
+ }
1144
+ else if (name.childCount > 1) {
1145
+ warnings.push([
1146
+ "extra-command_name-children",
1147
+ 'expected "command_name" node to only have one child but it has ' +
1148
+ name.childCount,
1149
+ ]);
1150
+ }
1151
+ const nameNode = name.firstChild;
1152
+ const nameWord = toWord(nameNode, curlCommand, warnings);
1153
+ const nameWordStr = nameWord.toString();
1154
+ const cmdNameShellToken = firstShellToken(nameWord);
1155
+ if (cmdNameShellToken) {
1156
+ // The most common reason for the command name to contain an expression
1157
+ // is probably users accidentally copying a $ from the shell prompt
1158
+ // without a space after it
1159
+ if (nameWordStr !== "$curl") {
1160
+ // TODO: or just assume it evaluates to "curl"?
1161
+ throw new CCError("expected command name to be a simple value but found a " +
1162
+ cmdNameShellToken.type +
1163
+ "\n" +
1164
+ underlineNode(cmdNameShellToken.syntaxNode, curlCommand));
1165
+ }
1166
+ }
1167
+ else if (nameWordStr.trim() !== "curl") {
1168
+ const c = nameWordStr.trim();
1169
+ if (!c) {
1170
+ throw new CCError("found command without a command_name\n" +
1171
+ underlineNode(nameNode, curlCommand));
1172
+ }
1173
+ throw new CCError('command should begin with "curl" but instead begins with ' +
1174
+ JSON.stringify(clip(c)) +
1175
+ "\n" +
1176
+ underlineNode(nameNode, curlCommand));
1177
+ }
1178
+ return nameWord;
1179
+ }
1180
+ function tokenize(curlCommand, warnings = []) {
1181
+ const ast = parser.parse(curlCommand);
1182
+ warnAboutErrorNodes(ast, curlCommand, warnings);
1183
+ // TODO: pass syntax nodes for each token downstream and use it to
1184
+ // highlight the problematic parts in warnings/errors so that it's clear
1185
+ // which command a warning/error is for
1186
+ const commandNodes = extractCommandNodes(ast, curlCommand, warnings);
1187
+ const commands = [];
1188
+ for (const [command, stdin, stdinFile] of commandNodes) {
1189
+ const [name, argv] = toNameAndArgv(command, curlCommand, warnings);
1190
+ commands.push([
1191
+ [
1192
+ nameToWord(name, curlCommand, warnings),
1193
+ ...argv.map((arg) => toWord(arg, curlCommand, warnings)),
1194
+ ],
1195
+ stdin,
1196
+ stdinFile,
1197
+ ]);
1198
+ }
1199
+ return commands;
1200
+ }
1201
+
1202
+ const CURLAUTH_BASIC = 1 << 0;
1203
+ const CURLAUTH_DIGEST = 1 << 1;
1204
+ const CURLAUTH_NEGOTIATE = 1 << 2;
1205
+ const CURLAUTH_NTLM = 1 << 3;
1206
+ const CURLAUTH_DIGEST_IE = 1 << 4;
1207
+ const CURLAUTH_NTLM_WB = 1 << 5;
1208
+ const CURLAUTH_BEARER = 1 << 6;
1209
+ const CURLAUTH_AWS_SIGV4 = 1 << 7;
1210
+ const CURLAUTH_ANY = ~CURLAUTH_DIGEST_IE;
1211
+ // This is this function
1212
+ // https://github.com/curl/curl/blob/curl-7_86_0/lib/http.c#L455
1213
+ // which is not the correct function, since it works on the response.
1214
+ //
1215
+ // Curl also filters out auth schemes it doesn't support,
1216
+ // https://github.com/curl/curl/blob/curl-7_86_0/lib/setopt.c#L970
1217
+ // but we "support" all of them, so we don't need to do that.
1218
+ function pickAuth(mask) {
1219
+ if (mask === CURLAUTH_ANY) {
1220
+ return "basic";
1221
+ }
1222
+ const auths = [
1223
+ [CURLAUTH_NEGOTIATE, "negotiate"],
1224
+ [CURLAUTH_BEARER, "bearer"],
1225
+ [CURLAUTH_DIGEST, "digest"],
1226
+ [CURLAUTH_NTLM, "ntlm"],
1227
+ [CURLAUTH_NTLM_WB, "ntlm-wb"],
1228
+ [CURLAUTH_BASIC, "basic"],
1229
+ // This check happens outside this function because we obviously
1230
+ // don't need to to specify --no-basic to use aws-sigv4
1231
+ // https://github.com/curl/curl/blob/curl-7_86_0/lib/setopt.c#L678-L679
1232
+ [CURLAUTH_AWS_SIGV4, "aws-sigv4"],
1233
+ ];
1234
+ for (const [auth, authName] of auths) {
1235
+ if (mask & auth) {
1236
+ return authName;
1237
+ }
1238
+ }
1239
+ return "none";
1240
+ }
1241
+
1242
+ // prettier-ignore
1243
+ const curlLongOpts = {
1244
+ // BEGIN EXTRACTED OPTIONS
1245
+ "url": { type: "string", name: "url" },
1246
+ "dns-ipv4-addr": { type: "string", name: "dns-ipv4-addr" },
1247
+ "dns-ipv6-addr": { type: "string", name: "dns-ipv6-addr" },
1248
+ "random-file": { type: "string", name: "random-file" },
1249
+ "egd-file": { type: "string", name: "egd-file" },
1250
+ "oauth2-bearer": { type: "string", name: "oauth2-bearer" },
1251
+ "connect-timeout": { type: "string", name: "connect-timeout" },
1252
+ "doh-url": { type: "string", name: "doh-url" },
1253
+ "ciphers": { type: "string", name: "ciphers" },
1254
+ "dns-interface": { type: "string", name: "dns-interface" },
1255
+ "disable-epsv": { type: "bool", name: "disable-epsv" },
1256
+ "no-disable-epsv": { type: "bool", name: "disable-epsv", expand: false },
1257
+ "disallow-username-in-url": { type: "bool", name: "disallow-username-in-url" },
1258
+ "no-disallow-username-in-url": { type: "bool", name: "disallow-username-in-url", expand: false },
1259
+ "epsv": { type: "bool", name: "epsv" },
1260
+ "no-epsv": { type: "bool", name: "epsv", expand: false },
1261
+ "dns-servers": { type: "string", name: "dns-servers" },
1262
+ "trace": { type: "string", name: "trace" },
1263
+ "npn": { type: "bool", name: "npn" },
1264
+ "no-npn": { type: "bool", name: "npn", expand: false },
1265
+ "trace-ascii": { type: "string", name: "trace-ascii" },
1266
+ "alpn": { type: "bool", name: "alpn" },
1267
+ "no-alpn": { type: "bool", name: "alpn", expand: false },
1268
+ "limit-rate": { type: "string", name: "limit-rate" },
1269
+ "rate": { type: "string", name: "rate" },
1270
+ "compressed": { type: "bool", name: "compressed" },
1271
+ "no-compressed": { type: "bool", name: "compressed", expand: false },
1272
+ "tr-encoding": { type: "bool", name: "tr-encoding" },
1273
+ "no-tr-encoding": { type: "bool", name: "tr-encoding", expand: false },
1274
+ "digest": { type: "bool", name: "digest" },
1275
+ "no-digest": { type: "bool", name: "digest", expand: false },
1276
+ "negotiate": { type: "bool", name: "negotiate" },
1277
+ "no-negotiate": { type: "bool", name: "negotiate", expand: false },
1278
+ "ntlm": { type: "bool", name: "ntlm" },
1279
+ "no-ntlm": { type: "bool", name: "ntlm", expand: false },
1280
+ "ntlm-wb": { type: "bool", name: "ntlm-wb" },
1281
+ "no-ntlm-wb": { type: "bool", name: "ntlm-wb", expand: false },
1282
+ "basic": { type: "bool", name: "basic" },
1283
+ "no-basic": { type: "bool", name: "basic", expand: false },
1284
+ "anyauth": { type: "bool", name: "anyauth" },
1285
+ "no-anyauth": { type: "bool", name: "anyauth", expand: false },
1286
+ "wdebug": { type: "bool", name: "wdebug" },
1287
+ "no-wdebug": { type: "bool", name: "wdebug", expand: false },
1288
+ "ftp-create-dirs": { type: "bool", name: "ftp-create-dirs" },
1289
+ "no-ftp-create-dirs": { type: "bool", name: "ftp-create-dirs", expand: false },
1290
+ "create-dirs": { type: "bool", name: "create-dirs" },
1291
+ "no-create-dirs": { type: "bool", name: "create-dirs", expand: false },
1292
+ "create-file-mode": { type: "string", name: "create-file-mode" },
1293
+ "max-redirs": { type: "string", name: "max-redirs" },
1294
+ "proxy-ntlm": { type: "bool", name: "proxy-ntlm" },
1295
+ "no-proxy-ntlm": { type: "bool", name: "proxy-ntlm", expand: false },
1296
+ "crlf": { type: "bool", name: "crlf" },
1297
+ "no-crlf": { type: "bool", name: "crlf", expand: false },
1298
+ "stderr": { type: "string", name: "stderr" },
1299
+ "aws-sigv4": { type: "string", name: "aws-sigv4" },
1300
+ "interface": { type: "string", name: "interface" },
1301
+ "krb": { type: "string", name: "krb" },
1302
+ "krb4": { type: "string", name: "krb" },
1303
+ "haproxy-protocol": { type: "bool", name: "haproxy-protocol" },
1304
+ "no-haproxy-protocol": { type: "bool", name: "haproxy-protocol", expand: false },
1305
+ "haproxy-clientip": { type: "string", name: "haproxy-clientip" },
1306
+ "max-filesize": { type: "string", name: "max-filesize" },
1307
+ "disable-eprt": { type: "bool", name: "disable-eprt" },
1308
+ "no-disable-eprt": { type: "bool", name: "disable-eprt", expand: false },
1309
+ "eprt": { type: "bool", name: "eprt" },
1310
+ "no-eprt": { type: "bool", name: "eprt", expand: false },
1311
+ "xattr": { type: "bool", name: "xattr" },
1312
+ "no-xattr": { type: "bool", name: "xattr", expand: false },
1313
+ "ftp-ssl": { type: "bool", name: "ssl" },
1314
+ "no-ftp-ssl": { type: "bool", name: "ssl", expand: false },
1315
+ "ssl": { type: "bool", name: "ssl" },
1316
+ "no-ssl": { type: "bool", name: "ssl", expand: false },
1317
+ "ftp-pasv": { type: "bool", name: "ftp-pasv" },
1318
+ "no-ftp-pasv": { type: "bool", name: "ftp-pasv", expand: false },
1319
+ "socks5": { type: "string", name: "socks5" },
1320
+ "tcp-nodelay": { type: "bool", name: "tcp-nodelay" },
1321
+ "no-tcp-nodelay": { type: "bool", name: "tcp-nodelay", expand: false },
1322
+ "proxy-digest": { type: "bool", name: "proxy-digest" },
1323
+ "no-proxy-digest": { type: "bool", name: "proxy-digest", expand: false },
1324
+ "proxy-basic": { type: "bool", name: "proxy-basic" },
1325
+ "no-proxy-basic": { type: "bool", name: "proxy-basic", expand: false },
1326
+ "retry": { type: "string", name: "retry" },
1327
+ "retry-connrefused": { type: "bool", name: "retry-connrefused" },
1328
+ "no-retry-connrefused": { type: "bool", name: "retry-connrefused", expand: false },
1329
+ "retry-delay": { type: "string", name: "retry-delay" },
1330
+ "retry-max-time": { type: "string", name: "retry-max-time" },
1331
+ "proxy-negotiate": { type: "bool", name: "proxy-negotiate" },
1332
+ "no-proxy-negotiate": { type: "bool", name: "proxy-negotiate", expand: false },
1333
+ "form-escape": { type: "bool", name: "form-escape" },
1334
+ "no-form-escape": { type: "bool", name: "form-escape", expand: false },
1335
+ "ftp-account": { type: "string", name: "ftp-account" },
1336
+ "proxy-anyauth": { type: "bool", name: "proxy-anyauth" },
1337
+ "no-proxy-anyauth": { type: "bool", name: "proxy-anyauth", expand: false },
1338
+ "trace-time": { type: "bool", name: "trace-time" },
1339
+ "no-trace-time": { type: "bool", name: "trace-time", expand: false },
1340
+ "ignore-content-length": { type: "bool", name: "ignore-content-length" },
1341
+ "no-ignore-content-length": { type: "bool", name: "ignore-content-length", expand: false },
1342
+ "ftp-skip-pasv-ip": { type: "bool", name: "ftp-skip-pasv-ip" },
1343
+ "no-ftp-skip-pasv-ip": { type: "bool", name: "ftp-skip-pasv-ip", expand: false },
1344
+ "ftp-method": { type: "string", name: "ftp-method" },
1345
+ "local-port": { type: "string", name: "local-port" },
1346
+ "socks4": { type: "string", name: "socks4" },
1347
+ "socks4a": { type: "string", name: "socks4a" },
1348
+ "ftp-alternative-to-user": { type: "string", name: "ftp-alternative-to-user" },
1349
+ "ftp-ssl-reqd": { type: "bool", name: "ssl-reqd" },
1350
+ "no-ftp-ssl-reqd": { type: "bool", name: "ssl-reqd", expand: false },
1351
+ "ssl-reqd": { type: "bool", name: "ssl-reqd" },
1352
+ "no-ssl-reqd": { type: "bool", name: "ssl-reqd", expand: false },
1353
+ "sessionid": { type: "bool", name: "sessionid" },
1354
+ "no-sessionid": { type: "bool", name: "sessionid", expand: false },
1355
+ "ftp-ssl-control": { type: "bool", name: "ftp-ssl-control" },
1356
+ "no-ftp-ssl-control": { type: "bool", name: "ftp-ssl-control", expand: false },
1357
+ "ftp-ssl-ccc": { type: "bool", name: "ftp-ssl-ccc" },
1358
+ "no-ftp-ssl-ccc": { type: "bool", name: "ftp-ssl-ccc", expand: false },
1359
+ "ftp-ssl-ccc-mode": { type: "string", name: "ftp-ssl-ccc-mode" },
1360
+ "libcurl": { type: "string", name: "libcurl" },
1361
+ "raw": { type: "bool", name: "raw" },
1362
+ "no-raw": { type: "bool", name: "raw", expand: false },
1363
+ "post301": { type: "bool", name: "post301" },
1364
+ "no-post301": { type: "bool", name: "post301", expand: false },
1365
+ "keepalive": { type: "bool", name: "keepalive" },
1366
+ "no-keepalive": { type: "bool", name: "keepalive", expand: false },
1367
+ "socks5-hostname": { type: "string", name: "socks5-hostname" },
1368
+ "keepalive-time": { type: "string", name: "keepalive-time" },
1369
+ "post302": { type: "bool", name: "post302" },
1370
+ "no-post302": { type: "bool", name: "post302", expand: false },
1371
+ "noproxy": { type: "string", name: "noproxy" },
1372
+ "socks5-gssapi-nec": { type: "bool", name: "socks5-gssapi-nec" },
1373
+ "no-socks5-gssapi-nec": { type: "bool", name: "socks5-gssapi-nec", expand: false },
1374
+ "proxy1.0": { type: "string", name: "proxy1.0" },
1375
+ "tftp-blksize": { type: "string", name: "tftp-blksize" },
1376
+ "mail-from": { type: "string", name: "mail-from" },
1377
+ "mail-rcpt": { type: "string", name: "mail-rcpt" },
1378
+ "ftp-pret": { type: "bool", name: "ftp-pret" },
1379
+ "no-ftp-pret": { type: "bool", name: "ftp-pret", expand: false },
1380
+ "proto": { type: "string", name: "proto" },
1381
+ "proto-redir": { type: "string", name: "proto-redir" },
1382
+ "resolve": { type: "string", name: "resolve" },
1383
+ "delegation": { type: "string", name: "delegation" },
1384
+ "mail-auth": { type: "string", name: "mail-auth" },
1385
+ "post303": { type: "bool", name: "post303" },
1386
+ "no-post303": { type: "bool", name: "post303", expand: false },
1387
+ "metalink": { type: "bool", name: "metalink" },
1388
+ "no-metalink": { type: "bool", name: "metalink", expand: false },
1389
+ "sasl-authzid": { type: "string", name: "sasl-authzid" },
1390
+ "sasl-ir": { type: "bool", name: "sasl-ir" },
1391
+ "no-sasl-ir": { type: "bool", name: "sasl-ir", expand: false },
1392
+ "test-event": { type: "bool", name: "test-event" },
1393
+ "no-test-event": { type: "bool", name: "test-event", expand: false },
1394
+ "unix-socket": { type: "string", name: "unix-socket" },
1395
+ "path-as-is": { type: "bool", name: "path-as-is" },
1396
+ "no-path-as-is": { type: "bool", name: "path-as-is", expand: false },
1397
+ "socks5-gssapi-service": { type: "string", name: "proxy-service-name" },
1398
+ "proxy-service-name": { type: "string", name: "proxy-service-name" },
1399
+ "service-name": { type: "string", name: "service-name" },
1400
+ "proto-default": { type: "string", name: "proto-default" },
1401
+ "expect100-timeout": { type: "string", name: "expect100-timeout" },
1402
+ "tftp-no-options": { type: "bool", name: "tftp-no-options" },
1403
+ "no-tftp-no-options": { type: "bool", name: "tftp-no-options", expand: false },
1404
+ "connect-to": { type: "string", name: "connect-to" },
1405
+ "abstract-unix-socket": { type: "string", name: "abstract-unix-socket" },
1406
+ "tls-max": { type: "string", name: "tls-max" },
1407
+ "suppress-connect-headers": { type: "bool", name: "suppress-connect-headers" },
1408
+ "no-suppress-connect-headers": { type: "bool", name: "suppress-connect-headers", expand: false },
1409
+ "compressed-ssh": { type: "bool", name: "compressed-ssh" },
1410
+ "no-compressed-ssh": { type: "bool", name: "compressed-ssh", expand: false },
1411
+ "happy-eyeballs-timeout-ms": { type: "string", name: "happy-eyeballs-timeout-ms" },
1412
+ "retry-all-errors": { type: "bool", name: "retry-all-errors" },
1413
+ "no-retry-all-errors": { type: "bool", name: "retry-all-errors", expand: false },
1414
+ "trace-ids": { type: "bool", name: "trace-ids" },
1415
+ "no-trace-ids": { type: "bool", name: "trace-ids", expand: false },
1416
+ "http1.0": { type: "bool", name: "http1.0" },
1417
+ "http1.1": { type: "bool", name: "http1.1" },
1418
+ "http2": { type: "bool", name: "http2" },
1419
+ "http2-prior-knowledge": { type: "bool", name: "http2-prior-knowledge" },
1420
+ "http3": { type: "bool", name: "http3" },
1421
+ "http3-only": { type: "bool", name: "http3-only" },
1422
+ "http0.9": { type: "bool", name: "http0.9" },
1423
+ "no-http0.9": { type: "bool", name: "http0.9", expand: false },
1424
+ "proxy-http2": { type: "bool", name: "proxy-http2" },
1425
+ "no-proxy-http2": { type: "bool", name: "proxy-http2", expand: false },
1426
+ "tlsv1": { type: "bool", name: "tlsv1" },
1427
+ "tlsv1.0": { type: "bool", name: "tlsv1.0" },
1428
+ "tlsv1.1": { type: "bool", name: "tlsv1.1" },
1429
+ "tlsv1.2": { type: "bool", name: "tlsv1.2" },
1430
+ "tlsv1.3": { type: "bool", name: "tlsv1.3" },
1431
+ "tls13-ciphers": { type: "string", name: "tls13-ciphers" },
1432
+ "proxy-tls13-ciphers": { type: "string", name: "proxy-tls13-ciphers" },
1433
+ "sslv2": { type: "bool", name: "sslv2" },
1434
+ "sslv3": { type: "bool", name: "sslv3" },
1435
+ "ipv4": { type: "bool", name: "ipv4" },
1436
+ "ipv6": { type: "bool", name: "ipv6" },
1437
+ "append": { type: "bool", name: "append" },
1438
+ "no-append": { type: "bool", name: "append", expand: false },
1439
+ "user-agent": { type: "string", name: "user-agent" },
1440
+ "cookie": { type: "string", name: "cookie" },
1441
+ "alt-svc": { type: "string", name: "alt-svc" },
1442
+ "hsts": { type: "string", name: "hsts" },
1443
+ "use-ascii": { type: "bool", name: "use-ascii" },
1444
+ "no-use-ascii": { type: "bool", name: "use-ascii", expand: false },
1445
+ "cookie-jar": { type: "string", name: "cookie-jar" },
1446
+ "continue-at": { type: "string", name: "continue-at" },
1447
+ "data": { type: "string", name: "data" },
1448
+ "data-raw": { type: "string", name: "data-raw" },
1449
+ "data-ascii": { type: "string", name: "data-ascii" },
1450
+ "data-binary": { type: "string", name: "data-binary" },
1451
+ "data-urlencode": { type: "string", name: "data-urlencode" },
1452
+ "json": { type: "string", name: "json" },
1453
+ "url-query": { type: "string", name: "url-query" },
1454
+ "dump-header": { type: "string", name: "dump-header" },
1455
+ "referer": { type: "string", name: "referer" },
1456
+ "cert": { type: "string", name: "cert" },
1457
+ "cacert": { type: "string", name: "cacert" },
1458
+ "cert-type": { type: "string", name: "cert-type" },
1459
+ "key": { type: "string", name: "key" },
1460
+ "key-type": { type: "string", name: "key-type" },
1461
+ "pass": { type: "string", name: "pass" },
1462
+ "engine": { type: "string", name: "engine" },
1463
+ "ca-native": { type: "bool", name: "ca-native" },
1464
+ "no-ca-native": { type: "bool", name: "ca-native", expand: false },
1465
+ "proxy-ca-native": { type: "bool", name: "proxy-ca-native" },
1466
+ "no-proxy-ca-native": { type: "bool", name: "proxy-ca-native", expand: false },
1467
+ "capath": { type: "string", name: "capath" },
1468
+ "pubkey": { type: "string", name: "pubkey" },
1469
+ "hostpubmd5": { type: "string", name: "hostpubmd5" },
1470
+ "hostpubsha256": { type: "string", name: "hostpubsha256" },
1471
+ "crlfile": { type: "string", name: "crlfile" },
1472
+ "tlsuser": { type: "string", name: "tlsuser" },
1473
+ "tlspassword": { type: "string", name: "tlspassword" },
1474
+ "tlsauthtype": { type: "string", name: "tlsauthtype" },
1475
+ "ssl-allow-beast": { type: "bool", name: "ssl-allow-beast" },
1476
+ "no-ssl-allow-beast": { type: "bool", name: "ssl-allow-beast", expand: false },
1477
+ "ssl-auto-client-cert": { type: "bool", name: "ssl-auto-client-cert" },
1478
+ "no-ssl-auto-client-cert": { type: "bool", name: "ssl-auto-client-cert", expand: false },
1479
+ "proxy-ssl-auto-client-cert": { type: "bool", name: "proxy-ssl-auto-client-cert" },
1480
+ "no-proxy-ssl-auto-client-cert": { type: "bool", name: "proxy-ssl-auto-client-cert", expand: false },
1481
+ "pinnedpubkey": { type: "string", name: "pinnedpubkey" },
1482
+ "proxy-pinnedpubkey": { type: "string", name: "proxy-pinnedpubkey" },
1483
+ "cert-status": { type: "bool", name: "cert-status" },
1484
+ "no-cert-status": { type: "bool", name: "cert-status", expand: false },
1485
+ "doh-cert-status": { type: "bool", name: "doh-cert-status" },
1486
+ "no-doh-cert-status": { type: "bool", name: "doh-cert-status", expand: false },
1487
+ "false-start": { type: "bool", name: "false-start" },
1488
+ "no-false-start": { type: "bool", name: "false-start", expand: false },
1489
+ "ssl-no-revoke": { type: "bool", name: "ssl-no-revoke" },
1490
+ "no-ssl-no-revoke": { type: "bool", name: "ssl-no-revoke", expand: false },
1491
+ "ssl-revoke-best-effort": { type: "bool", name: "ssl-revoke-best-effort" },
1492
+ "no-ssl-revoke-best-effort": { type: "bool", name: "ssl-revoke-best-effort", expand: false },
1493
+ "tcp-fastopen": { type: "bool", name: "tcp-fastopen" },
1494
+ "no-tcp-fastopen": { type: "bool", name: "tcp-fastopen", expand: false },
1495
+ "proxy-tlsuser": { type: "string", name: "proxy-tlsuser" },
1496
+ "proxy-tlspassword": { type: "string", name: "proxy-tlspassword" },
1497
+ "proxy-tlsauthtype": { type: "string", name: "proxy-tlsauthtype" },
1498
+ "proxy-cert": { type: "string", name: "proxy-cert" },
1499
+ "proxy-cert-type": { type: "string", name: "proxy-cert-type" },
1500
+ "proxy-key": { type: "string", name: "proxy-key" },
1501
+ "proxy-key-type": { type: "string", name: "proxy-key-type" },
1502
+ "proxy-pass": { type: "string", name: "proxy-pass" },
1503
+ "proxy-ciphers": { type: "string", name: "proxy-ciphers" },
1504
+ "proxy-crlfile": { type: "string", name: "proxy-crlfile" },
1505
+ "proxy-ssl-allow-beast": { type: "bool", name: "proxy-ssl-allow-beast" },
1506
+ "no-proxy-ssl-allow-beast": { type: "bool", name: "proxy-ssl-allow-beast", expand: false },
1507
+ "login-options": { type: "string", name: "login-options" },
1508
+ "proxy-cacert": { type: "string", name: "proxy-cacert" },
1509
+ "proxy-capath": { type: "string", name: "proxy-capath" },
1510
+ "proxy-insecure": { type: "bool", name: "proxy-insecure" },
1511
+ "no-proxy-insecure": { type: "bool", name: "proxy-insecure", expand: false },
1512
+ "proxy-tlsv1": { type: "bool", name: "proxy-tlsv1" },
1513
+ "socks5-basic": { type: "bool", name: "socks5-basic" },
1514
+ "no-socks5-basic": { type: "bool", name: "socks5-basic", expand: false },
1515
+ "socks5-gssapi": { type: "bool", name: "socks5-gssapi" },
1516
+ "no-socks5-gssapi": { type: "bool", name: "socks5-gssapi", expand: false },
1517
+ "etag-save": { type: "string", name: "etag-save" },
1518
+ "etag-compare": { type: "string", name: "etag-compare" },
1519
+ "curves": { type: "string", name: "curves" },
1520
+ "fail": { type: "bool", name: "fail" },
1521
+ "no-fail": { type: "bool", name: "fail", expand: false },
1522
+ "fail-early": { type: "bool", name: "fail-early" },
1523
+ "no-fail-early": { type: "bool", name: "fail-early", expand: false },
1524
+ "styled-output": { type: "bool", name: "styled-output" },
1525
+ "no-styled-output": { type: "bool", name: "styled-output", expand: false },
1526
+ "mail-rcpt-allowfails": { type: "bool", name: "mail-rcpt-allowfails" },
1527
+ "no-mail-rcpt-allowfails": { type: "bool", name: "mail-rcpt-allowfails", expand: false },
1528
+ "fail-with-body": { type: "bool", name: "fail-with-body" },
1529
+ "no-fail-with-body": { type: "bool", name: "fail-with-body", expand: false },
1530
+ "remove-on-error": { type: "bool", name: "remove-on-error" },
1531
+ "no-remove-on-error": { type: "bool", name: "remove-on-error", expand: false },
1532
+ "form": { type: "string", name: "form" },
1533
+ "form-string": { type: "string", name: "form-string" },
1534
+ "globoff": { type: "bool", name: "globoff" },
1535
+ "no-globoff": { type: "bool", name: "globoff", expand: false },
1536
+ "get": { type: "bool", name: "get" },
1537
+ "no-get": { type: "bool", name: "get", expand: false },
1538
+ "request-target": { type: "string", name: "request-target" },
1539
+ "help": { type: "bool", name: "help" },
1540
+ "no-help": { type: "bool", name: "help", expand: false },
1541
+ "header": { type: "string", name: "header" },
1542
+ "proxy-header": { type: "string", name: "proxy-header" },
1543
+ "include": { type: "bool", name: "include" },
1544
+ "no-include": { type: "bool", name: "include", expand: false },
1545
+ "head": { type: "bool", name: "head" },
1546
+ "no-head": { type: "bool", name: "head", expand: false },
1547
+ "junk-session-cookies": { type: "bool", name: "junk-session-cookies" },
1548
+ "no-junk-session-cookies": { type: "bool", name: "junk-session-cookies", expand: false },
1549
+ "remote-header-name": { type: "bool", name: "remote-header-name" },
1550
+ "no-remote-header-name": { type: "bool", name: "remote-header-name", expand: false },
1551
+ "insecure": { type: "bool", name: "insecure" },
1552
+ "no-insecure": { type: "bool", name: "insecure", expand: false },
1553
+ "doh-insecure": { type: "bool", name: "doh-insecure" },
1554
+ "no-doh-insecure": { type: "bool", name: "doh-insecure", expand: false },
1555
+ "config": { type: "string", name: "config" },
1556
+ "list-only": { type: "bool", name: "list-only" },
1557
+ "no-list-only": { type: "bool", name: "list-only", expand: false },
1558
+ "location": { type: "bool", name: "location" },
1559
+ "no-location": { type: "bool", name: "location", expand: false },
1560
+ "location-trusted": { type: "bool", name: "location-trusted" },
1561
+ "no-location-trusted": { type: "bool", name: "location-trusted", expand: false },
1562
+ "max-time": { type: "string", name: "max-time" },
1563
+ "manual": { type: "bool", name: "manual" },
1564
+ "no-manual": { type: "bool", name: "manual", expand: false },
1565
+ "netrc": { type: "bool", name: "netrc" },
1566
+ "no-netrc": { type: "bool", name: "netrc", expand: false },
1567
+ "netrc-optional": { type: "bool", name: "netrc-optional" },
1568
+ "no-netrc-optional": { type: "bool", name: "netrc-optional", expand: false },
1569
+ "netrc-file": { type: "string", name: "netrc-file" },
1570
+ "buffer": { type: "bool", name: "buffer" },
1571
+ "no-buffer": { type: "bool", name: "buffer", expand: false },
1572
+ "output": { type: "string", name: "output" },
1573
+ "remote-name": { type: "bool", name: "remote-name" },
1574
+ "no-remote-name": { type: "bool", name: "remote-name", expand: false },
1575
+ "remote-name-all": { type: "bool", name: "remote-name-all" },
1576
+ "no-remote-name-all": { type: "bool", name: "remote-name-all", expand: false },
1577
+ "output-dir": { type: "string", name: "output-dir" },
1578
+ "clobber": { type: "bool", name: "clobber" },
1579
+ "no-clobber": { type: "bool", name: "clobber", expand: false },
1580
+ "proxytunnel": { type: "bool", name: "proxytunnel" },
1581
+ "no-proxytunnel": { type: "bool", name: "proxytunnel", expand: false },
1582
+ "ftp-port": { type: "string", name: "ftp-port" },
1583
+ "disable": { type: "bool", name: "disable" },
1584
+ "no-disable": { type: "bool", name: "disable", expand: false },
1585
+ "quote": { type: "string", name: "quote" },
1586
+ "range": { type: "string", name: "range" },
1587
+ "remote-time": { type: "bool", name: "remote-time" },
1588
+ "no-remote-time": { type: "bool", name: "remote-time", expand: false },
1589
+ "silent": { type: "bool", name: "silent" },
1590
+ "no-silent": { type: "bool", name: "silent", expand: false },
1591
+ "show-error": { type: "bool", name: "show-error" },
1592
+ "no-show-error": { type: "bool", name: "show-error", expand: false },
1593
+ "telnet-option": { type: "string", name: "telnet-option" },
1594
+ "upload-file": { type: "string", name: "upload-file" },
1595
+ "user": { type: "string", name: "user" },
1596
+ "proxy-user": { type: "string", name: "proxy-user" },
1597
+ "verbose": { type: "bool", name: "verbose" },
1598
+ "no-verbose": { type: "bool", name: "verbose", expand: false },
1599
+ "version": { type: "bool", name: "version" },
1600
+ "no-version": { type: "bool", name: "version", expand: false },
1601
+ "write-out": { type: "string", name: "write-out" },
1602
+ "proxy": { type: "string", name: "proxy" },
1603
+ "preproxy": { type: "string", name: "preproxy" },
1604
+ "request": { type: "string", name: "request" },
1605
+ "speed-limit": { type: "string", name: "speed-limit" },
1606
+ "speed-time": { type: "string", name: "speed-time" },
1607
+ "time-cond": { type: "string", name: "time-cond" },
1608
+ "parallel": { type: "bool", name: "parallel" },
1609
+ "no-parallel": { type: "bool", name: "parallel", expand: false },
1610
+ "parallel-max": { type: "string", name: "parallel-max" },
1611
+ "parallel-immediate": { type: "bool", name: "parallel-immediate" },
1612
+ "no-parallel-immediate": { type: "bool", name: "parallel-immediate", expand: false },
1613
+ "progress-bar": { type: "bool", name: "progress-bar" },
1614
+ "no-progress-bar": { type: "bool", name: "progress-bar", expand: false },
1615
+ "progress-meter": { type: "bool", name: "progress-meter" },
1616
+ "no-progress-meter": { type: "bool", name: "progress-meter", expand: false },
1617
+ "next": { type: "bool", name: "next" },
1618
+ // END EXTRACTED OPTIONS
1619
+ // These are options that curl used to have.
1620
+ // Those that don't conflict with the current options are supported by curlconverter.
1621
+ // TODO: curl's --long-options can be shortened.
1622
+ // For example if curl used to only have a single option, "--blah" then
1623
+ // "--bla" "--bl" and "--b" all used to be valid options as well. If later
1624
+ // "--blaz" was added, suddenly those 3 shortened options are removed (because
1625
+ // they are now ambiguous).
1626
+ // https://github.com/curlconverter/curlconverter/pull/280#issuecomment-931241328
1627
+ port: { type: "string", name: "port", removed: "7.3" },
1628
+ // These are now shoretened forms of --upload-file and --continue-at
1629
+ //upload: { type: "bool", name: "upload", removed: "7.7" },
1630
+ //continue: { type: "bool", name: "continue", removed: "7.9" },
1631
+ "ftp-ascii": { type: "bool", name: "use-ascii", removed: "7.10.7" },
1632
+ "3p-url": { type: "string", name: "3p-url", removed: "7.16.0" },
1633
+ "3p-user": { type: "string", name: "3p-user", removed: "7.16.0" },
1634
+ "3p-quote": { type: "string", name: "3p-quote", removed: "7.16.0" },
1635
+ "http2.0": { type: "bool", name: "http2", removed: "7.36.0" },
1636
+ "no-http2.0": { type: "bool", name: "http2", removed: "7.36.0" },
1637
+ "telnet-options": { type: "string", name: "telnet-option", removed: "7.49.0" },
1638
+ "http-request": { type: "string", name: "request", removed: "7.49.0" },
1639
+ // --socks is now an ambiguous shortening of --socks4, --socks5 and a bunch more
1640
+ //socks: { type: "string", name: "socks5", removed: "7.49.0" },
1641
+ "capath ": { type: "string", name: "capath", removed: "7.49.0" },
1642
+ ftpport: { type: "string", name: "ftp-port", removed: "7.49.0" },
1643
+ environment: { type: "bool", name: "environment", removed: "7.54.1" },
1644
+ // These never had any effect
1645
+ "no-tlsv1": { type: "bool", name: "tlsv1", removed: "7.54.1" },
1646
+ "no-tlsv1.2": { type: "bool", name: "tlsv1.2", removed: "7.54.1" },
1647
+ "no-http2-prior-knowledge": { type: "bool", name: "http2-prior-knowledge", removed: "7.54.1" },
1648
+ "no-ipv6": { type: "bool", name: "ipv6", removed: "7.54.1" },
1649
+ "no-ipv4": { type: "bool", name: "ipv4", removed: "7.54.1" },
1650
+ "no-sslv2": { type: "bool", name: "sslv2", removed: "7.54.1" },
1651
+ "no-tlsv1.0": { type: "bool", name: "tlsv1.0", removed: "7.54.1" },
1652
+ "no-tlsv1.1": { type: "bool", name: "tlsv1.1", removed: "7.54.1" },
1653
+ "no-sslv3": { type: "bool", name: "sslv3", removed: "7.54.1" },
1654
+ "no-http1.0": { type: "bool", name: "http1.0", removed: "7.54.1" },
1655
+ "no-next": { type: "bool", name: "next", removed: "7.54.1" },
1656
+ "no-tlsv1.3": { type: "bool", name: "tlsv1.3", removed: "7.54.1" },
1657
+ "no-environment": { type: "bool", name: "environment", removed: "7.54.1" },
1658
+ "no-http1.1": { type: "bool", name: "http1.1", removed: "7.54.1" },
1659
+ "no-proxy-tlsv1": { type: "bool", name: "proxy-tlsv1", removed: "7.54.1" },
1660
+ "no-http2": { type: "bool", name: "http2", removed: "7.54.1" },
1661
+ };
1662
+ // curl lets you not type the full argument as long as it's unambiguous.
1663
+ // So --sil instead of --silent is okay, --s is not.
1664
+ const curlLongOptsShortened = {};
1665
+ for (const [opt, val] of Object.entries(curlLongOpts)) {
1666
+ const expand = "expand" in val ? val.expand : true;
1667
+ const removed = "removed" in val ? val.removed : false;
1668
+ if (expand && !removed) {
1669
+ for (let i = 1; i < opt.length; i++) {
1670
+ const shortenedOpt = opt.slice(0, i);
1671
+ if (!Object.prototype.hasOwnProperty.call(curlLongOptsShortened, shortenedOpt)) {
1672
+ if (!Object.prototype.hasOwnProperty.call(curlLongOpts, shortenedOpt)) {
1673
+ curlLongOptsShortened[shortenedOpt] = val;
1674
+ }
1675
+ }
1676
+ else {
1677
+ // If more than one option shortens to this, it's ambiguous
1678
+ curlLongOptsShortened[shortenedOpt] = null;
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+ // Arguments which are supported by all generators, because they're
1684
+ // easy to implement or because they're handled by upstream code and
1685
+ // affect something that's easy to implement.
1686
+ const COMMON_SUPPORTED_ARGS = [
1687
+ "url",
1688
+ "proto-default",
1689
+ // Method
1690
+ "request",
1691
+ "get",
1692
+ "head",
1693
+ "no-head",
1694
+ // Headers
1695
+ "header",
1696
+ "user-agent",
1697
+ "referer",
1698
+ "range",
1699
+ "time-cond",
1700
+ "cookie",
1701
+ "oauth2-bearer",
1702
+ // Basic Auth
1703
+ "user",
1704
+ "basic",
1705
+ "no-basic",
1706
+ // Data
1707
+ "data",
1708
+ "data-raw",
1709
+ "data-ascii",
1710
+ "data-binary",
1711
+ "data-urlencode",
1712
+ "json",
1713
+ "url-query",
1714
+ // Trivial support for globoff means controlling whether or not
1715
+ // backslash-escaped [] {} will have the backslash removed.
1716
+ "globoff",
1717
+ // curl will exit if it finds auth credentials in the URL with this option,
1718
+ // we remove it from the URL and emit a warning instead.
1719
+ "disallow-username-in-url",
1720
+ // TODO: --compressed is already the default for some runtimes, in
1721
+ // which case we might have to only warn that --no-compressed isn't supported.
1722
+ ];
1723
+ function toBoolean(opt) {
1724
+ if (opt.startsWith("no-disable-")) {
1725
+ return true;
1726
+ }
1727
+ if (opt.startsWith("disable-") || opt.startsWith("no-")) {
1728
+ return false;
1729
+ }
1730
+ return true;
1731
+ }
1732
+ // prettier-ignore
1733
+ const curlShortOpts = {
1734
+ // BEGIN EXTRACTED SHORT OPTIONS
1735
+ "0": "http1.0",
1736
+ "1": "tlsv1",
1737
+ "2": "sslv2",
1738
+ "3": "sslv3",
1739
+ "4": "ipv4",
1740
+ "6": "ipv6",
1741
+ "a": "append",
1742
+ "A": "user-agent",
1743
+ "b": "cookie",
1744
+ "B": "use-ascii",
1745
+ "c": "cookie-jar",
1746
+ "C": "continue-at",
1747
+ "d": "data",
1748
+ "D": "dump-header",
1749
+ "e": "referer",
1750
+ "E": "cert",
1751
+ "f": "fail",
1752
+ "F": "form",
1753
+ "g": "globoff",
1754
+ "G": "get",
1755
+ "h": "help",
1756
+ "H": "header",
1757
+ "i": "include",
1758
+ "I": "head",
1759
+ "j": "junk-session-cookies",
1760
+ "J": "remote-header-name",
1761
+ "k": "insecure",
1762
+ "K": "config",
1763
+ "l": "list-only",
1764
+ "L": "location",
1765
+ "m": "max-time",
1766
+ "M": "manual",
1767
+ "n": "netrc",
1768
+ "N": "no-buffer",
1769
+ "o": "output",
1770
+ "O": "remote-name",
1771
+ "p": "proxytunnel",
1772
+ "P": "ftp-port",
1773
+ "q": "disable",
1774
+ "Q": "quote",
1775
+ "r": "range",
1776
+ "R": "remote-time",
1777
+ "s": "silent",
1778
+ "S": "show-error",
1779
+ "t": "telnet-option",
1780
+ "T": "upload-file",
1781
+ "u": "user",
1782
+ "U": "proxy-user",
1783
+ "v": "verbose",
1784
+ "V": "version",
1785
+ "w": "write-out",
1786
+ "x": "proxy",
1787
+ "X": "request",
1788
+ "Y": "speed-limit",
1789
+ "y": "speed-time",
1790
+ "z": "time-cond",
1791
+ "Z": "parallel",
1792
+ "#": "progress-bar",
1793
+ ":": "next",
1794
+ // END EXTRACTED SHORT OPTIONS
1795
+ };
1796
+ const changedShortOpts = {
1797
+ p: "used to be short for --port <port> (a since-deleted flag) until curl 7.3",
1798
+ // TODO: some of these might be renamed options
1799
+ t: "used to be short for --upload (a since-deleted boolean flag) until curl 7.7",
1800
+ c: "used to be short for --continue (a since-deleted boolean flag) until curl 7.9",
1801
+ // TODO: did -@ actually work?
1802
+ "@": "used to be short for --create-dirs until curl 7.10.7",
1803
+ Z: "used to be short for --max-redirs <num> until curl 7.10.7",
1804
+ 9: "used to be short for --crlf until curl 7.10.8",
1805
+ 8: "used to be short for --stderr <file> until curl 7.10.8",
1806
+ 7: "used to be short for --interface <name> until curl 7.10.8",
1807
+ 6: "used to be short for --krb <level> (which itself used to be --krb4 <level>) until curl 7.10.8",
1808
+ // TODO: did these short options ever actually work?
1809
+ 5: "used to be another way to specify the url until curl 7.10.8",
1810
+ "*": "used to be another way to specify the url until curl 7.49.0",
1811
+ "~": "used to be short for --xattr until curl 7.49.0",
1812
+ };
1813
+ // type Satisfies<T, U extends T> = void;
1814
+ // type AssertSubsetKeys = Satisfies<
1815
+ // keyof typeof curlLongOpts | "authtype" | "authArgs",
1816
+ // keyof OperationConfig
1817
+ // >;
1818
+ // These options can be specified more than once, they
1819
+ // are always returned as a list.
1820
+ // For all other options, if you specify it more than once
1821
+ // curl will use the last one.
1822
+ const canBeList = new Set([
1823
+ "authArgs",
1824
+ "connect-to",
1825
+ "cookie",
1826
+ "data",
1827
+ "form",
1828
+ "header",
1829
+ "hsts",
1830
+ "mail-rcpt",
1831
+ "output",
1832
+ "proxy-header",
1833
+ "quote",
1834
+ "resolve",
1835
+ "telnet-option",
1836
+ "upload-file",
1837
+ "url-query",
1838
+ "url",
1839
+ ]);
1840
+ function checkSupported(global, lookup, longArg, supportedOpts) {
1841
+ if (supportedOpts && !supportedOpts.has(longArg.name)) {
1842
+ // TODO: better message. include generator name?
1843
+ warnf(global, [
1844
+ longArg.name,
1845
+ lookup +
1846
+ " is not a supported option" +
1847
+ (longArg.removed ? ", it was removed in curl " + longArg.removed : ""),
1848
+ ]);
1849
+ }
1850
+ }
1851
+ function pushProp(obj, prop, value) {
1852
+ if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
1853
+ obj[prop] = [];
1854
+ }
1855
+ obj[prop].push(value);
1856
+ return obj;
1857
+ }
1858
+ function pushArgValue(global, config, argName, value) {
1859
+ // Note: cli.ts assumes that the property names on OperationConfig
1860
+ // are the same as the passed in argument in an error message, so
1861
+ // if you do something like
1862
+ // echo curl example.com | curlconverter - --data-raw foo
1863
+ // The error message will say
1864
+ // "if you pass --stdin or -, you can't also pass --data"
1865
+ // instead of "--data-raw".
1866
+ switch (argName) {
1867
+ case "data":
1868
+ case "data-ascii":
1869
+ return pushProp(config, "data", ["data", value]);
1870
+ case "data-binary":
1871
+ return pushProp(config, "data", [
1872
+ // Unless it's a file, --data-binary works the same as --data
1873
+ value.startsWith("@") ? "binary" : "data",
1874
+ value,
1875
+ ]);
1876
+ case "data-raw":
1877
+ return pushProp(config, "data", [
1878
+ // Unless it's a file, --data-raw works the same as --data
1879
+ value.startsWith("@") ? "raw" : "data",
1880
+ value,
1881
+ ]);
1882
+ case "data-urlencode":
1883
+ return pushProp(config, "data", ["urlencode", value]);
1884
+ case "json":
1885
+ config.json = true;
1886
+ return pushProp(config, "data", ["json", value]);
1887
+ case "url-query":
1888
+ if (value.startsWith("+")) {
1889
+ return pushProp(config, "url-query", ["raw", value.slice(1)]);
1890
+ }
1891
+ return pushProp(config, "url-query", ["urlencode", value]);
1892
+ case "form":
1893
+ return pushProp(config, "form", { value, type: "form" });
1894
+ case "form-string":
1895
+ return pushProp(config, "form", { value, type: "string" });
1896
+ case "aws-sigv4":
1897
+ pushProp(config, "authArgs", [argName, true]); // error reporting
1898
+ config.authtype |= CURLAUTH_AWS_SIGV4;
1899
+ break;
1900
+ case "oauth2-bearer":
1901
+ pushProp(config, "authArgs", [argName, true]); // error reporting
1902
+ config.authtype |= CURLAUTH_BEARER;
1903
+ break;
1904
+ case "unix-socket":
1905
+ case "abstract-unix-socket":
1906
+ // Ignore distinction
1907
+ // TODO: this makes the error message wrong
1908
+ // TODO: what's the difference?
1909
+ pushProp(config, "unix-socket", value);
1910
+ break;
1911
+ case "trace":
1912
+ case "trace-ascii":
1913
+ case "stderr":
1914
+ case "libcurl":
1915
+ case "config":
1916
+ case "parallel-max":
1917
+ global[argName] = value;
1918
+ break;
1919
+ case "language": // --language is a curlconverter specific option
1920
+ global[argName] = value.toString();
1921
+ return;
1922
+ }
1923
+ return pushProp(config, argName, value);
1924
+ }
1925
+ // Might create a new config
1926
+ function setArgValue(global, config, argName, toggle) {
1927
+ switch (argName) {
1928
+ case "digest":
1929
+ pushProp(config, "authArgs", [argName, toggle]); // error reporting
1930
+ if (toggle) {
1931
+ config.authtype |= CURLAUTH_DIGEST;
1932
+ }
1933
+ else {
1934
+ config.authtype &= ~CURLAUTH_DIGEST;
1935
+ }
1936
+ break;
1937
+ case "negotiate":
1938
+ pushProp(config, "authArgs", [argName, toggle]); // error reporting
1939
+ if (toggle) {
1940
+ config.authtype |= CURLAUTH_NEGOTIATE;
1941
+ }
1942
+ else {
1943
+ config.authtype &= ~CURLAUTH_NEGOTIATE;
1944
+ }
1945
+ break;
1946
+ case "ntlm":
1947
+ pushProp(config, "authArgs", [argName, toggle]); // error reporting
1948
+ if (toggle) {
1949
+ config.authtype |= CURLAUTH_NTLM;
1950
+ }
1951
+ else {
1952
+ config.authtype &= ~CURLAUTH_NTLM;
1953
+ }
1954
+ break;
1955
+ case "ntlm-wb":
1956
+ pushProp(config, "authArgs", [argName, toggle]); // error reporting
1957
+ if (toggle) {
1958
+ config.authtype |= CURLAUTH_NTLM_WB;
1959
+ }
1960
+ else {
1961
+ config.authtype &= ~CURLAUTH_NTLM_WB;
1962
+ }
1963
+ break;
1964
+ case "basic":
1965
+ pushProp(config, "authArgs", [argName, toggle]); // error reporting
1966
+ if (toggle) {
1967
+ config.authtype |= CURLAUTH_BASIC;
1968
+ }
1969
+ else {
1970
+ config.authtype &= ~CURLAUTH_BASIC;
1971
+ }
1972
+ break;
1973
+ case "anyauth":
1974
+ pushProp(config, "authArgs", [argName, toggle]); // error reporting
1975
+ if (toggle) {
1976
+ config.authtype = CURLAUTH_ANY;
1977
+ }
1978
+ break;
1979
+ case "location":
1980
+ config["location"] = toggle;
1981
+ break;
1982
+ case "location-trusted":
1983
+ config["location"] = toggle;
1984
+ config["location-trusted"] = toggle;
1985
+ break;
1986
+ case "verbose":
1987
+ case "version":
1988
+ case "trace-time":
1989
+ case "test-event":
1990
+ case "progress-bar":
1991
+ case "progress-meter":
1992
+ case "fail-early":
1993
+ case "styled-output":
1994
+ case "help":
1995
+ case "silent":
1996
+ case "show-error":
1997
+ case "parallel":
1998
+ case "parallel-immediate":
1999
+ case "stdin": // --stdin or - is a curlconverter specific option
2000
+ global[argName] = toggle;
2001
+ break;
2002
+ case "next":
2003
+ // curl ignores --next if the last url node doesn't have a url
2004
+ if (toggle &&
2005
+ config.url &&
2006
+ config.url.length > 0 &&
2007
+ config.url.length >= (config["upload-file"] || []).length &&
2008
+ config.url.length >= (config.output || []).length) {
2009
+ config = { authtype: CURLAUTH_BASIC };
2010
+ global.configs.push(config);
2011
+ }
2012
+ break;
2013
+ default:
2014
+ config[argName] = toggle;
2015
+ }
2016
+ return config;
2017
+ }
2018
+ function parseArgs(args, longOpts = curlLongOpts, shortenedLongOpts = curlLongOptsShortened, shortOpts = curlShortOpts, supportedOpts, warnings = []) {
2019
+ let config = { authtype: CURLAUTH_BASIC };
2020
+ const global = { configs: [config], warnings };
2021
+ for (let i = 1, stillflags = true; i < args.length; i++) {
2022
+ const arg = args[i];
2023
+ if (stillflags && arg.startsWith("-")) {
2024
+ if (eq(arg, "--")) {
2025
+ /* This indicates the end of the flags and thus enables the
2026
+ following (URL) argument to start with -. */
2027
+ stillflags = false;
2028
+ }
2029
+ else if (arg.startsWith("--")) {
2030
+ const shellToken = firstShellToken(arg);
2031
+ if (shellToken) {
2032
+ // TODO: if there's any text after the "--" or after the variable
2033
+ // we could narrow it down.
2034
+ throw new CCError("this " +
2035
+ shellToken.type +
2036
+ " could " +
2037
+ (shellToken.type === "command" ? "return" : "be") +
2038
+ " anything\n" +
2039
+ underlineNode(shellToken.syntaxNode));
2040
+ }
2041
+ const argStr = arg.toString();
2042
+ const lookup = argStr.slice(2);
2043
+ let longArg = shortenedLongOpts[lookup];
2044
+ if (typeof longArg === "undefined") {
2045
+ longArg = longOpts[lookup];
2046
+ }
2047
+ if (longArg === null) {
2048
+ throw new CCError("option " + argStr + ": is ambiguous");
2049
+ }
2050
+ if (typeof longArg === "undefined") {
2051
+ // TODO: extract a list of deleted arguments to check here
2052
+ throw new CCError("option " + argStr + ": is unknown");
2053
+ }
2054
+ if (longArg.type === "string") {
2055
+ i++;
2056
+ if (i >= args.length) {
2057
+ throw new CCError("option " + argStr + ": requires parameter");
2058
+ }
2059
+ pushArgValue(global, config, longArg.name, args[i]);
2060
+ }
2061
+ else {
2062
+ config = setArgValue(global, config, longArg.name, toBoolean(argStr.slice(2))); // TODO: all shortened args work correctly?
2063
+ }
2064
+ checkSupported(global, argStr, longArg, supportedOpts);
2065
+ }
2066
+ else {
2067
+ // Short option. These can look like
2068
+ // -X POST -> {request: 'POST'}
2069
+ // or
2070
+ // -XPOST -> {request: 'POST'}
2071
+ // or multiple options
2072
+ // -ABCX POST -> {A: true, B: true, C: true, request: 'POST'}
2073
+ // or multiple options and a value for the last one
2074
+ // -ABCXPOST -> {A: true, B: true, C: true, request: 'POST'}
2075
+ // "-" passed to curl as an argument raises an error,
2076
+ // curlconverter's command line uses it to read from stdin
2077
+ if (arg.length === 1) {
2078
+ if (Object.prototype.hasOwnProperty.call(shortOpts, "")) {
2079
+ const shortFor = shortOpts[""];
2080
+ const longArg = longOpts[shortFor];
2081
+ if (longArg === null) {
2082
+ throw new CCError("option -: is unknown");
2083
+ }
2084
+ config = setArgValue(global, config, longArg.name, toBoolean(shortFor));
2085
+ }
2086
+ else {
2087
+ throw new CCError("option -: is unknown");
2088
+ }
2089
+ }
2090
+ for (let j = 1; j < arg.length; j++) {
2091
+ const jthChar = arg.get(j);
2092
+ if (typeof jthChar !== "string") {
2093
+ // A bash variable in the middle of a short option
2094
+ throw new CCError("this " +
2095
+ jthChar.type +
2096
+ " could " +
2097
+ (jthChar.type === "command" ? "return" : "be") +
2098
+ " anything\n" +
2099
+ underlineNode(jthChar.syntaxNode));
2100
+ }
2101
+ if (!has(shortOpts, jthChar)) {
2102
+ if (has(changedShortOpts, jthChar)) {
2103
+ throw new CCError("option " + arg + ": " + changedShortOpts[jthChar]);
2104
+ }
2105
+ // TODO: there are a few deleted short options we could report
2106
+ throw new CCError("option " + arg + ": is unknown");
2107
+ }
2108
+ const lookup = jthChar;
2109
+ const shortFor = shortOpts[lookup];
2110
+ const longArg = longOpts[shortFor];
2111
+ if (longArg === null) {
2112
+ // This could happen if curlShortOpts points to a renamed option or has a typo
2113
+ throw new CCError("ambiguous short option -" + jthChar);
2114
+ }
2115
+ if (longArg.type === "string") {
2116
+ let val;
2117
+ if (j + 1 < arg.length) {
2118
+ // treat -XPOST as -X POST
2119
+ val = arg.slice(j + 1);
2120
+ j = arg.length;
2121
+ }
2122
+ else if (i + 1 < args.length) {
2123
+ i++;
2124
+ val = args[i];
2125
+ }
2126
+ else {
2127
+ throw new CCError("option " + arg.toString() + ": requires parameter");
2128
+ }
2129
+ pushArgValue(global, config, longArg.name, val);
2130
+ }
2131
+ else {
2132
+ // Use shortFor because -N is short for --no-buffer
2133
+ // and we want to end up with {buffer: false}
2134
+ config = setArgValue(global, config, longArg.name, toBoolean(shortFor));
2135
+ }
2136
+ if (lookup) {
2137
+ checkSupported(global, "-" + lookup, longArg, supportedOpts);
2138
+ }
2139
+ }
2140
+ }
2141
+ }
2142
+ else {
2143
+ if (typeof arg !== "string" &&
2144
+ arg.tokens.length &&
2145
+ typeof arg.tokens[0] !== "string") {
2146
+ const isOrBeginsWith = arg.tokens.length === 1 ? "is" : "begins with";
2147
+ warnings.push([
2148
+ "ambiguous argument",
2149
+ "argument " +
2150
+ isOrBeginsWith +
2151
+ " a " +
2152
+ arg.tokens[0].type +
2153
+ ", assuming it's a URL\n" +
2154
+ underlineNode(arg.tokens[0].syntaxNode),
2155
+ ]);
2156
+ }
2157
+ pushArgValue(global, config, "url", arg);
2158
+ }
2159
+ }
2160
+ for (const cfg of global.configs) {
2161
+ for (const [arg, values] of Object.entries(cfg)) {
2162
+ if (Array.isArray(values) && !canBeList.has(arg)) {
2163
+ cfg[arg] = values[values.length - 1];
2164
+ }
2165
+ }
2166
+ }
2167
+ return global;
2168
+ }
2169
+
2170
+ // https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Standard_request_fields
2171
+ // and then searched for "#" in the RFCs that define each header
2172
+ const COMMA_SEPARATED = new Set([
2173
+ "A-IM",
2174
+ "Accept",
2175
+ "Accept-Charset",
2176
+ // "Accept-Datetime",
2177
+ "Accept-Encoding",
2178
+ "Accept-Language",
2179
+ // "Access-Control-Request-Method",
2180
+ "Access-Control-Request-Headers",
2181
+ // TODO: auth-scheme [ 1*SP ( token68 / #auth-param ) ]
2182
+ // "Authorization",
2183
+ "Cache-Control",
2184
+ "Connection",
2185
+ "Content-Encoding",
2186
+ // "Content-Length",
2187
+ // "Content-MD5",
2188
+ // "Content-Type", // semicolon
2189
+ // "Cookie", // semicolon
2190
+ // "Date",
2191
+ "Expect",
2192
+ "Forwarded",
2193
+ // "From",
2194
+ // "Host",
2195
+ // "HTTP2-Settings",
2196
+ "If-Match",
2197
+ // "If-Modified-Since",
2198
+ "If-None-Match",
2199
+ // "If-Range",
2200
+ // "If-Unmodified-Since",
2201
+ // "Max-Forwards",
2202
+ // "Origin",
2203
+ // "Pragma",
2204
+ // "Prefer", // semicolon
2205
+ // "Proxy-Authorization",
2206
+ "Range",
2207
+ // "Referer",
2208
+ "TE",
2209
+ "Trailer",
2210
+ "Transfer-Encoding",
2211
+ // "User-Agent",
2212
+ "Upgrade",
2213
+ "Via",
2214
+ "Warning",
2215
+ ].map((h) => h.toLowerCase()));
2216
+ const SEMICOLON_SEPARATED = new Set(["Content-Type", "Cookie", "Prefer"].map((h) => h.toLowerCase()));
2217
+ class Headers {
2218
+ constructor(headerArgs, warnings = []) {
2219
+ let headers = [];
2220
+ if (headerArgs) {
2221
+ for (const header of headerArgs) {
2222
+ if (header.startsWith("@")) {
2223
+ warnings.push([
2224
+ "header-file",
2225
+ "passing a file for --header/-H is not supported: " +
2226
+ JSON.stringify(header.toString()),
2227
+ ]);
2228
+ continue;
2229
+ }
2230
+ if (header.includes(":")) {
2231
+ const [name, value] = header.split(":", 2);
2232
+ const nameToken = firstShellToken(name);
2233
+ if (nameToken) {
2234
+ warnings.push([
2235
+ "header-expression",
2236
+ "ignoring " +
2237
+ nameToken.type +
2238
+ " in header name\n" +
2239
+ underlineNode(nameToken.syntaxNode),
2240
+ ]);
2241
+ }
2242
+ // TODO: whitespace-only headers are treated incosistently.
2243
+ // curl -H 'Hosts: ' example.com sends the header
2244
+ // curl -H 'User-Agent: ' example.com doesn't
2245
+ const hasValue = value && value.trim().toBool();
2246
+ const headerValue = hasValue ? value.removeFirstChar(" ") : null;
2247
+ headers.push([name, headerValue]);
2248
+ }
2249
+ else if (header.includes(";")) {
2250
+ const [name] = header.split(";", 2);
2251
+ headers.push([name, new Word()]);
2252
+ }
2253
+ else ;
2254
+ }
2255
+ }
2256
+ this.lowercase =
2257
+ headers.length > 0 && headers.every((h) => eq(h[0], h[0].toLowerCase()));
2258
+ // Handle repeated headers
2259
+ // For Cookie and Accept, merge the values using ';' and ',' respectively
2260
+ // For other headers, warn about the repeated header
2261
+ const uniqueHeaders = {};
2262
+ for (const [name, value] of headers) {
2263
+ // TODO: something better, at least warn that variable is ignored
2264
+ const lowerName = name.toLowerCase().toString();
2265
+ if (!uniqueHeaders[lowerName]) {
2266
+ uniqueHeaders[lowerName] = [];
2267
+ }
2268
+ uniqueHeaders[lowerName].push([name, value]);
2269
+ }
2270
+ headers = [];
2271
+ for (const [lowerName, repeatedHeaders] of Object.entries(uniqueHeaders)) {
2272
+ if (repeatedHeaders.length === 1) {
2273
+ headers.push(repeatedHeaders[0]);
2274
+ continue;
2275
+ }
2276
+ // If they're all null, just use the first one
2277
+ if (repeatedHeaders.every((h) => h[1] === null)) {
2278
+ const lastRepeat = repeatedHeaders[repeatedHeaders.length - 1];
2279
+ // Warn users if some are capitalized differently
2280
+ if (new Set(repeatedHeaders.map((h) => h[0])).size > 1) {
2281
+ warnings.push([
2282
+ "repeated-header",
2283
+ `"${lastRepeat[0]}" header unset ${repeatedHeaders.length} times`,
2284
+ ]);
2285
+ }
2286
+ headers.push(lastRepeat);
2287
+ continue;
2288
+ }
2289
+ // Otherwise there's at least one non-null value, so we can ignore the nulls
2290
+ // TODO: if the values of the repeated headers are the same, just use the first one
2291
+ // 'content-type': 'application/json; application/json',
2292
+ // doesn't really make sense
2293
+ const nonEmptyHeaders = repeatedHeaders.filter((h) => h[1] !== null);
2294
+ if (nonEmptyHeaders.length === 1) {
2295
+ headers.push(nonEmptyHeaders[0]);
2296
+ continue;
2297
+ }
2298
+ let mergeChar = "";
2299
+ if (COMMA_SEPARATED.has(lowerName)) {
2300
+ mergeChar = ", ";
2301
+ }
2302
+ else if (SEMICOLON_SEPARATED.has(lowerName)) {
2303
+ mergeChar = "; ";
2304
+ }
2305
+ if (mergeChar) {
2306
+ const merged = joinWords(nonEmptyHeaders.map((h) => h[1]), mergeChar);
2307
+ warnings.push([
2308
+ "repeated-header",
2309
+ `merged ${nonEmptyHeaders.length} "${nonEmptyHeaders[nonEmptyHeaders.length - 1][0]}" headers together with "${mergeChar.trim()}"`,
2310
+ ]);
2311
+ headers.push([nonEmptyHeaders[0][0], merged]);
2312
+ continue;
2313
+ }
2314
+ warnings.push([
2315
+ "repeated-header",
2316
+ `found ${nonEmptyHeaders.length} "${nonEmptyHeaders[nonEmptyHeaders.length - 1][0]}" headers, only the last one will be sent`,
2317
+ ]);
2318
+ headers = headers.concat(nonEmptyHeaders);
2319
+ }
2320
+ this.headers = headers;
2321
+ }
2322
+ get length() {
2323
+ return this.headers.length;
2324
+ }
2325
+ *[Symbol.iterator]() {
2326
+ for (const h of this.headers) {
2327
+ yield h;
2328
+ }
2329
+ }
2330
+ // Gets the first header, matching case-insensitively
2331
+ get(header) {
2332
+ const lookup = header.toLowerCase();
2333
+ for (const [h, v] of this.headers) {
2334
+ if (h.toLowerCase().toString() === lookup) {
2335
+ return v;
2336
+ }
2337
+ }
2338
+ return undefined;
2339
+ }
2340
+ getContentType() {
2341
+ const contentTypeHeader = this.get("content-type");
2342
+ if (!contentTypeHeader) {
2343
+ return contentTypeHeader;
2344
+ }
2345
+ return contentTypeHeader.split(";")[0].trim().toString();
2346
+ }
2347
+ has(header) {
2348
+ const lookup = header.toLowerCase();
2349
+ for (const h of this.headers) {
2350
+ if (eq(h[0].toLowerCase(), lookup)) {
2351
+ return true;
2352
+ }
2353
+ }
2354
+ return false;
2355
+ }
2356
+ // Doesn't overwrite existing headers
2357
+ setIfMissing(header, value) {
2358
+ if (this.has(header)) {
2359
+ return false;
2360
+ }
2361
+ if (this.lowercase) {
2362
+ header = header.toLowerCase();
2363
+ }
2364
+ const k = typeof header === "string" ? new Word(header) : header;
2365
+ const v = typeof value === "string" ? new Word(value) : value;
2366
+ this.headers.push([k, v]);
2367
+ return true;
2368
+ }
2369
+ prependIfMissing(header, value) {
2370
+ if (this.has(header)) {
2371
+ return false;
2372
+ }
2373
+ if (this.lowercase) {
2374
+ header = header.toLowerCase();
2375
+ }
2376
+ const k = typeof header === "string" ? new Word(header) : header;
2377
+ const v = typeof value === "string" ? new Word(value) : value;
2378
+ this.headers.unshift([k, v]);
2379
+ return true;
2380
+ }
2381
+ set(header, value) {
2382
+ if (this.lowercase) {
2383
+ header = header.toLowerCase();
2384
+ }
2385
+ const k = typeof header === "string" ? new Word(header) : header;
2386
+ const v = typeof value === "string" ? new Word(value) : value;
2387
+ // keep it in the same place if we overwrite
2388
+ const searchHeader = k.toLowerCase().toString();
2389
+ for (let i = 0; i < this.headers.length; i++) {
2390
+ if (eq(this.headers[i][0].toLowerCase(), searchHeader)) {
2391
+ this.headers[i][1] = v;
2392
+ return;
2393
+ }
2394
+ }
2395
+ this.headers.push([k, v]);
2396
+ }
2397
+ delete(header) {
2398
+ const lookup = header.toLowerCase();
2399
+ for (let i = this.headers.length - 1; i >= 0; i--) {
2400
+ if (this.headers[i][0].toLowerCase().toString() === lookup) {
2401
+ this.headers.splice(i, 1);
2402
+ }
2403
+ }
2404
+ }
2405
+ // TODO: doesn't this skip the next element after deleting?
2406
+ clearNulls() {
2407
+ for (let i = this.headers.length - 1; i >= 0; i--) {
2408
+ if (this.headers[i][1] === null) {
2409
+ this.headers.splice(i, 1);
2410
+ }
2411
+ }
2412
+ }
2413
+ // TODO: shouldn't be used
2414
+ count(header) {
2415
+ let count = 0;
2416
+ const lookup = header.toLowerCase();
2417
+ for (const h of this.headers || []) {
2418
+ if (h[0].toLowerCase().toString() === lookup) {
2419
+ count += 1;
2420
+ }
2421
+ }
2422
+ return count;
2423
+ }
2424
+ toBool() {
2425
+ return this.headers.length > 0 && this.headers.some((h) => h[1] !== null);
2426
+ }
2427
+ }
2428
+ function parseCookiesStrict(cookieString) {
2429
+ const cookies = [];
2430
+ for (let cookie of cookieString.split(";")) {
2431
+ cookie = cookie.replace(/^ /, "");
2432
+ const [name, value] = cookie.split("=", 2);
2433
+ if (value === undefined) {
2434
+ return null;
2435
+ }
2436
+ cookies.push([name, value]);
2437
+ }
2438
+ if (new Set(cookies.map((c) => c[0])).size !== cookies.length) {
2439
+ return null;
2440
+ }
2441
+ return cookies;
2442
+ }
2443
+ function parseCookies(cookieString) {
2444
+ const cookies = [];
2445
+ for (let cookie of cookieString.split(";")) {
2446
+ cookie = cookie.trim();
2447
+ if (!cookie) {
2448
+ continue;
2449
+ }
2450
+ const [name, value] = cookie.split("=", 2);
2451
+ cookies.push([name.trim(), (value || "").trim()]);
2452
+ }
2453
+ if (new Set(cookies.map((c) => c[0])).size !== cookies.length) {
2454
+ return null;
2455
+ }
2456
+ return cookies;
2457
+ }
2458
+
2459
+ // https://github.com/curl/curl/blob/curl-7_88_1/src/tool_urlglob.c#L327
2460
+ const MAX_IP6LEN = 128;
2461
+ function isIpv6(glob) {
2462
+ if (glob.length > MAX_IP6LEN) {
2463
+ return false;
2464
+ }
2465
+ // TODO: curl tries to parse the glob as a hostname.
2466
+ return !glob.includes("-");
2467
+ }
2468
+ function warnAboutGlobs(global, url) {
2469
+ // Find any glob expressions in the URL and underline them
2470
+ let prev = "";
2471
+ for (let i = 0; i < url.length; i++) {
2472
+ const cur = url[i];
2473
+ if (cur === "[" && prev !== "\\") {
2474
+ let j = i + 1;
2475
+ while (j < url.length && url[j] !== "]") {
2476
+ j++;
2477
+ }
2478
+ if (j < url.length && url[j] === "]") {
2479
+ const glob = url.slice(i, j + 1);
2480
+ // could be ipv6 address
2481
+ if (!isIpv6(glob)) {
2482
+ warnf(global, [
2483
+ "glob-in-url",
2484
+ `globs in the URL are not supported:\n` +
2485
+ `${url}\n` +
2486
+ " ".repeat(i) +
2487
+ "^".repeat(glob.length),
2488
+ ]);
2489
+ }
2490
+ prev = "";
2491
+ }
2492
+ else {
2493
+ // No closing bracket
2494
+ warnf(global, [
2495
+ "unbalanced-glob",
2496
+ "bracket doesn't have a closing bracket:\n" +
2497
+ `${url}\n` +
2498
+ `${" ".repeat(i)}^`,
2499
+ ]);
2500
+ return; // malformed URL, stop looking for globs
2501
+ }
2502
+ }
2503
+ else if (cur === "{" && prev !== "\\") {
2504
+ let j = i + 1;
2505
+ while (j < url.length && url[j] !== "}") {
2506
+ j++;
2507
+ }
2508
+ if (j < url.length && url[j] === "}") {
2509
+ const glob = url.slice(i, j + 1);
2510
+ warnf(global, [
2511
+ "glob-in-url",
2512
+ `globs in the URL are not supported:\n` +
2513
+ `${url}\n` +
2514
+ " ".repeat(i) +
2515
+ "^".repeat(glob.length),
2516
+ ]);
2517
+ prev = "";
2518
+ }
2519
+ else {
2520
+ // No closing bracket
2521
+ warnf(global, [
2522
+ "unbalanced-glob",
2523
+ "bracket doesn't have a closing bracket:\n" +
2524
+ `${url}\n` +
2525
+ `${" ".repeat(i)}^`,
2526
+ ]);
2527
+ return; // malformed URL, stop looking for globs
2528
+ }
2529
+ }
2530
+ prev = cur;
2531
+ }
2532
+ }
2533
+ function parseurl(global, config, url) {
2534
+ var _a;
2535
+ // This is curl's parseurl()
2536
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L1144
2537
+ // Except we want to accept all URLs.
2538
+ // curl further validates URLs in curl_url_get()
2539
+ // https://github.com/curl/curl/blob/curl-7_86_0/lib/urlapi.c#L1374
2540
+ const u = {
2541
+ scheme: new Word(),
2542
+ host: new Word(),
2543
+ port: new Word(),
2544
+ path: new Word(),
2545
+ query: new Word(),
2546
+ fragment: new Word(), // with leading '#'
2547
+ };
2548
+ // Remove url glob escapes
2549
+ // https://github.com/curl/curl/blob/curl-7_87_0/src/tool_urlglob.c#L395-L398
2550
+ if (!config.globoff) {
2551
+ if (url.isString()) {
2552
+ warnAboutGlobs(global, url.toString());
2553
+ }
2554
+ url = url.replace(/\\([[\]{}])/g, "$1");
2555
+ }
2556
+ // Prepend "http"/"https" if the scheme is missing.
2557
+ // RFC 3986 3.1 says
2558
+ // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
2559
+ // but curl will accept a digit/plus/minus/dot in the first character
2560
+ // curl will also accept a url with one / like http:/localhost
2561
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L960
2562
+ let schemeMatch = null;
2563
+ if (url.tokens.length && typeof url.tokens[0] === "string") {
2564
+ schemeMatch = url.tokens[0].match(/^([a-zA-Z0-9+-.]*):\/\/*/);
2565
+ }
2566
+ if (schemeMatch) {
2567
+ const [schemeAndSlashes, scheme] = schemeMatch;
2568
+ u.scheme = new Word(scheme.toLowerCase());
2569
+ url = url.slice(schemeAndSlashes.length);
2570
+ }
2571
+ else {
2572
+ // curl defaults to https://
2573
+ // we don't because most libraries won't downgrade to http
2574
+ // if you ask for https, unlike curl.
2575
+ // TODO: handle file:// scheme
2576
+ u.scheme = (_a = config["proto-default"]) !== null && _a !== void 0 ? _a : new Word("http");
2577
+ }
2578
+ if (!eq(u.scheme, "http") && !eq(u.scheme, "https")) {
2579
+ warnf(global, ["bad-scheme", `Protocol "${u.scheme}" not supported`]);
2580
+ }
2581
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L992
2582
+ const hostMatch = url.indexOfFirstChar("/?#");
2583
+ if (hostMatch !== -1) {
2584
+ u.host = url.slice(0, hostMatch);
2585
+ // TODO: u.path might end up empty if indexOfFirstChar found ?#
2586
+ u.path = url.slice(hostMatch); // keep leading '/' in .path
2587
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L1024
2588
+ const fragmentIndex = u.path.indexOf("#");
2589
+ const queryIndex = u.path.indexOf("?");
2590
+ if (fragmentIndex !== -1) {
2591
+ u.fragment = u.path.slice(fragmentIndex);
2592
+ if (queryIndex !== -1 && queryIndex < fragmentIndex) {
2593
+ u.query = u.path.slice(queryIndex, fragmentIndex);
2594
+ u.path = u.path.slice(0, queryIndex);
2595
+ }
2596
+ else {
2597
+ u.path = u.path.slice(0, fragmentIndex);
2598
+ }
2599
+ }
2600
+ else if (queryIndex !== -1) {
2601
+ u.query = u.path.slice(queryIndex);
2602
+ u.path = u.path.slice(0, queryIndex);
2603
+ }
2604
+ }
2605
+ else {
2606
+ u.host = url;
2607
+ }
2608
+ // parse username:password@hostname
2609
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L1083
2610
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L460
2611
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/url.c#L2827
2612
+ const authMatch = u.host.indexOf("@");
2613
+ if (authMatch !== -1) {
2614
+ const auth = u.host.slice(0, authMatch);
2615
+ u.host = u.host.slice(authMatch + 1); // throw away '@'
2616
+ if (!config["disallow-username-in-url"]) {
2617
+ u.auth = auth;
2618
+ if (auth.includes(":")) {
2619
+ [u.user, u.password] = auth.split(":", 2);
2620
+ }
2621
+ else {
2622
+ u.user = auth;
2623
+ u.password = new Word(); // if there's no ':', curl will append it
2624
+ }
2625
+ }
2626
+ else {
2627
+ // Curl will exit if this is the case, but we just remove it from the URL
2628
+ warnf(global, [
2629
+ "login-denied",
2630
+ `Found auth in URL but --disallow-username-in-url was passed: ${auth.toString()}`,
2631
+ ]);
2632
+ }
2633
+ }
2634
+ // TODO: need to extract port first
2635
+ // hostname_check()
2636
+ // https://github.com/curl/curl/blob/curl-7_86_0/lib/urlapi.c#L572
2637
+ // if (!u.host) {
2638
+ // warnf(global, [
2639
+ // "no-host",
2640
+ // "Found empty host in URL: " + JSON.stringify(url),
2641
+ // ]);
2642
+ // } else if (u.host.startsWith("[")) {
2643
+ // if (!u.host.endsWith("]")) {
2644
+ // warnf(global, [
2645
+ // "bad-host",
2646
+ // "Found invalid IPv6 address in URL: " + JSON.stringify(url),
2647
+ // ]);
2648
+ // } else {
2649
+ // const firstWeirdCharacter = u.host.match(/[^0123456789abcdefABCDEF:.]/);
2650
+ // // %zone_id
2651
+ // if (firstWeirdCharacter && firstWeirdCharacter[0] !== "%") {
2652
+ // warnf(global, [
2653
+ // "bad-host",
2654
+ // "Found invalid IPv6 address in URL: " + JSON.stringify(url),
2655
+ // ]);
2656
+ // }
2657
+ // }
2658
+ // } else {
2659
+ // const firstInvalidCharacter = u.host.match(
2660
+ // /[\r\n\t/:#?!@{}[\]\\$'"^`*<>=;,]/
2661
+ // );
2662
+ // if (firstInvalidCharacter) {
2663
+ // warnf(global, [
2664
+ // "bad-host",
2665
+ // "Found invalid character " +
2666
+ // JSON.stringify(firstInvalidCharacter[0]) +
2667
+ // " in URL: " +
2668
+ // JSON.stringify(url),
2669
+ // ]);
2670
+ // }
2671
+ // }
2672
+ return u;
2673
+ }
2674
+
2675
+ // Match Python's urllib.parse.quote() behavior
2676
+ // https://github.com/python/cpython/blob/3.11/Lib/urllib/parse.py#L826
2677
+ // curl and Python let you send non-ASCII characters by encoding each UTF-8 byte.
2678
+ // TODO: ignore hex case?
2679
+ function _percentEncode(s) {
2680
+ return [...UTF8encoder.encode(s)]
2681
+ .map((b) => {
2682
+ if (
2683
+ // A-Z
2684
+ (b >= 0x41 && b <= 0x5a) ||
2685
+ // a-z
2686
+ (b >= 0x61 && b <= 0x7a) ||
2687
+ // 0-9
2688
+ (b >= 0x30 && b <= 0x39) ||
2689
+ // -._~
2690
+ b === 0x2d ||
2691
+ b === 0x2e ||
2692
+ b === 0x5f ||
2693
+ b === 0x7e) {
2694
+ return String.fromCharCode(b);
2695
+ }
2696
+ return "%" + b.toString(16).toUpperCase().padStart(2, "0");
2697
+ })
2698
+ .join("");
2699
+ }
2700
+ function percentEncode(s) {
2701
+ const newTokens = [];
2702
+ for (const token of s.tokens) {
2703
+ if (typeof token === "string") {
2704
+ newTokens.push(_percentEncode(token));
2705
+ }
2706
+ else {
2707
+ newTokens.push(token);
2708
+ }
2709
+ }
2710
+ return new Word(newTokens);
2711
+ }
2712
+ function percentEncodePlus(s) {
2713
+ const newTokens = [];
2714
+ for (const token of s.tokens) {
2715
+ if (typeof token === "string") {
2716
+ newTokens.push(_percentEncode(token).replace(/%20/g, "+"));
2717
+ }
2718
+ else {
2719
+ newTokens.push(token);
2720
+ }
2721
+ }
2722
+ return new Word(newTokens);
2723
+ }
2724
+ // Reimplements decodeURIComponent but ignores variables/commands
2725
+ function wordDecodeURIComponent(s) {
2726
+ const newTokens = [];
2727
+ for (const token of s.tokens) {
2728
+ if (typeof token === "string") {
2729
+ newTokens.push(decodeURIComponent(token));
2730
+ }
2731
+ else {
2732
+ newTokens.push(token);
2733
+ }
2734
+ }
2735
+ return new Word(newTokens);
2736
+ }
2737
+ // if url is 'example.com?' the s is ''
2738
+ // if url is 'example.com' the s is null
2739
+ function parseQueryString(s) {
2740
+ if (!s || s.isEmpty()) {
2741
+ return [null, null];
2742
+ }
2743
+ const asList = [];
2744
+ for (const param of s.split("&")) {
2745
+ // Most software libraries don't let you distinguish between a=&b= and a&b,
2746
+ // so if we get an `a&b`-type query string, don't bother.
2747
+ if (!param.includes("=")) {
2748
+ return [null, null];
2749
+ }
2750
+ const [key, val] = param.split("=", 2);
2751
+ let decodedKey;
2752
+ let decodedVal;
2753
+ try {
2754
+ // https://url.spec.whatwg.org/#urlencoded-parsing
2755
+ // recommends replacing + with space before decoding.
2756
+ decodedKey = wordDecodeURIComponent(key.replace(/\+/g, " "));
2757
+ decodedVal = wordDecodeURIComponent(val.replace(/\+/g, " "));
2758
+ }
2759
+ catch (e) {
2760
+ if (e instanceof URIError) {
2761
+ // Query string contains invalid percent encoded characters,
2762
+ // we cannot properly convert it.
2763
+ return [null, null];
2764
+ }
2765
+ throw e;
2766
+ }
2767
+ // If the query string doesn't round-trip, we cannot properly convert it.
2768
+ // TODO: this is a bit Python-specific, ideally we would check how each runtime/library
2769
+ // percent-encodes query strings. For example, a %27 character in the input query
2770
+ // string will be decoded to a ' but won't be re-encoded into a %27 by encodeURIComponent
2771
+ const roundTripKey = percentEncode(decodedKey);
2772
+ const roundTripVal = percentEncode(decodedVal);
2773
+ // If the original data used %20 instead of + (what requests will send), that's close enough
2774
+ if ((!eq(roundTripKey, key) && !eq(roundTripKey.replace(/%20/g, "+"), key)) ||
2775
+ (!eq(roundTripVal, val) && !eq(roundTripVal.replace(/%20/g, "+"), val))) {
2776
+ return [null, null];
2777
+ }
2778
+ asList.push([decodedKey, decodedVal]);
2779
+ }
2780
+ // Group keys
2781
+ const keyWords = {};
2782
+ const uniqueKeys = {};
2783
+ let prevKey = null;
2784
+ for (const [key, val] of asList) {
2785
+ const keyStr = key.toString(); // TODO: do this better
2786
+ if (prevKey === keyStr) {
2787
+ uniqueKeys[keyStr].push(val);
2788
+ }
2789
+ else if (!Object.prototype.hasOwnProperty.call(uniqueKeys, keyStr)) {
2790
+ uniqueKeys[keyStr] = [val];
2791
+ keyWords[keyStr] = key;
2792
+ }
2793
+ else {
2794
+ // If there's a repeated key with a different key between
2795
+ // one of its repetitions, there is no way to represent
2796
+ // this query string as a dictionary.
2797
+ return [asList, null];
2798
+ }
2799
+ prevKey = keyStr;
2800
+ }
2801
+ // Convert lists with 1 element to the element
2802
+ const asDict = [];
2803
+ for (const [keyStr, val] of Object.entries(uniqueKeys)) {
2804
+ asDict.push([keyWords[keyStr], val.length === 1 ? val[0] : val]);
2805
+ }
2806
+ return [asList, asDict];
2807
+ }
2808
+
2809
+ function parseDetails(formParam, p, ptr, supported, warnings) {
2810
+ while (ptr < p.length && p.charAt(ptr) === ";") {
2811
+ ptr += 1;
2812
+ while (ptr < p.length && isSpace(p.charAt(ptr))) {
2813
+ ptr += 1;
2814
+ }
2815
+ if (ptr >= p.length) {
2816
+ return formParam;
2817
+ }
2818
+ const value = p.slice(ptr);
2819
+ if (value.startsWith("type=")) {
2820
+ // TODO: the syntax for type= is more complicated
2821
+ [formParam.contentType, ptr] = getParamWord(p, ptr + 5, warnings);
2822
+ }
2823
+ else if (value.startsWith("filename=")) {
2824
+ const [filename, filenameEnd] = getParamWord(p, ptr + 9, warnings);
2825
+ ptr = filenameEnd;
2826
+ if (supported.filename) {
2827
+ formParam.filename = filename;
2828
+ }
2829
+ else {
2830
+ warnings.push([
2831
+ "unsupported-form-detail",
2832
+ "Field file name not allowed here: " + filename.toString(),
2833
+ ]);
2834
+ }
2835
+ }
2836
+ else if (value.startsWith("encoder=")) {
2837
+ const [encoder, encoderEnd] = getParamWord(p, ptr + 8, warnings);
2838
+ ptr = encoderEnd;
2839
+ if (supported.encoder) {
2840
+ formParam.encoder = encoder;
2841
+ }
2842
+ else {
2843
+ warnings.push([
2844
+ "unsupported-form-detail",
2845
+ "Field encoder not allowed here: " + encoder.toString(),
2846
+ ]);
2847
+ }
2848
+ }
2849
+ else if (value.startsWith("headers=")) {
2850
+ // TODO: more complicated because of header files
2851
+ const [headers, headersEnd] = getParamWord(p, ptr + 8, warnings);
2852
+ ptr = headersEnd;
2853
+ if (supported.headers) {
2854
+ formParam.headers = headers;
2855
+ }
2856
+ else {
2857
+ warnings.push([
2858
+ "unsupported-form-detail",
2859
+ "Field headers not allowed here: " + headers.toString(),
2860
+ ]);
2861
+ }
2862
+ }
2863
+ else {
2864
+ // TODO: it would be more consistent for curl to skip until the first "=", then
2865
+ // getParamWord, because quoting a ; in an unknown value breaks values that
2866
+ // come after it, e.g.:
2867
+ // curl -F 'myname=myvalue;bfilename="f;oo";filename=oeu' localhost:8888
2868
+ const unknown = getParamWord(p, ptr, warnings);
2869
+ const unknownEnd = unknown[1];
2870
+ ptr = unknownEnd;
2871
+ warnings.push([
2872
+ "unknown-form-detail",
2873
+ "skip unknown form field: " + value.toString(),
2874
+ ]);
2875
+ }
2876
+ }
2877
+ return formParam;
2878
+ }
2879
+ function isSpace(c) {
2880
+ // Implements the following macro from curl:
2881
+ // #define ISBLANK(x) (((x) == ' ') || ((x) == '\t'))
2882
+ // #define ISSPACE(x) (ISBLANK(x) || (((x) >= 0xa) && ((x) <= 0x0d)))
2883
+ return (typeof c === "string" &&
2884
+ (c === " " || c === "\t" || (c >= "\n" && c <= "\r")));
2885
+ }
2886
+ function getParamWord(p, start, warnings) {
2887
+ let ptr = start;
2888
+ if (p.charAt(ptr) === '"') {
2889
+ ptr += 1;
2890
+ const parts = [];
2891
+ while (ptr < p.length) {
2892
+ let curChar = p.charAt(ptr);
2893
+ if (curChar === "\\") {
2894
+ if (ptr + 1 < p.length) {
2895
+ const nextChar = p.charAt(ptr + 1);
2896
+ if (nextChar === '"' || nextChar === "\\") {
2897
+ ptr += 1;
2898
+ curChar = p.charAt(ptr);
2899
+ }
2900
+ }
2901
+ }
2902
+ else if (curChar === '"') {
2903
+ ptr += 1;
2904
+ let trailingData = false;
2905
+ while (ptr < p.length && p.charAt(ptr) !== ";") {
2906
+ if (!isSpace(p.charAt(ptr))) {
2907
+ trailingData = true;
2908
+ }
2909
+ ptr += 1;
2910
+ }
2911
+ if (trailingData) {
2912
+ warnings.push([
2913
+ "trailing-form-data",
2914
+ "Trailing data after quoted form parameter",
2915
+ ]);
2916
+ }
2917
+ return [new Word(parts), ptr];
2918
+ }
2919
+ parts.push(curChar);
2920
+ ptr += 1;
2921
+ }
2922
+ }
2923
+ let sepIdx = p.indexOf(";", start);
2924
+ if (sepIdx === -1) {
2925
+ sepIdx = p.length;
2926
+ }
2927
+ return [p.slice(start, sepIdx), sepIdx];
2928
+ }
2929
+ function getParamPart(formParam, p, ptr, supported, warnings) {
2930
+ while (ptr < p.length && isSpace(p.charAt(ptr))) {
2931
+ ptr += 1;
2932
+ }
2933
+ const [content, contentEnd] = getParamWord(p, ptr, warnings);
2934
+ formParam.content = content;
2935
+ parseDetails(formParam, p, contentEnd, supported, warnings);
2936
+ return formParam;
2937
+ }
2938
+ // TODO: https://curl.se/docs/manpage.html#-F
2939
+ // https://github.com/curl/curl/blob/curl-7_88_1/src/tool_formparse.c
2940
+ // -F is a complicated option to parse.
2941
+ function parseForm(form, warnings) {
2942
+ const multipartUploads = [];
2943
+ let depth = 0;
2944
+ for (const multipartArgument of form) {
2945
+ const isString = multipartArgument.type === "string";
2946
+ if (!multipartArgument.value.includes("=")) {
2947
+ throw new CCError('invalid value for --form/-F, missing "=": ' +
2948
+ JSON.stringify(multipartArgument.value.toString()));
2949
+ }
2950
+ const [name, value] = multipartArgument.value.split("=", 2);
2951
+ const formParam = { name };
2952
+ if (!isString && value.charAt(0) === "(") {
2953
+ depth += 1;
2954
+ warnings.push([
2955
+ "nested-form",
2956
+ 'Nested form data with "=(" is not supported, it will be flattened',
2957
+ ]);
2958
+ getParamPart(formParam, value, 1, {
2959
+ headers: true,
2960
+ }, warnings);
2961
+ }
2962
+ else if (!isString && name.length === 0 && eq(value, ")")) {
2963
+ depth -= 1;
2964
+ if (depth < 0) {
2965
+ throw new CCError("no multipart to terminate: " +
2966
+ JSON.stringify(multipartArgument.value.toString()));
2967
+ }
2968
+ }
2969
+ else if (!isString && value.charAt(0) === "@") {
2970
+ // TODO: there can be multiple files separated by a comma
2971
+ getParamPart(formParam, value, 1, {
2972
+ filename: true,
2973
+ encoder: true,
2974
+ headers: true,
2975
+ }, warnings);
2976
+ formParam.contentFile = formParam.content;
2977
+ delete formParam.content;
2978
+ if (formParam.filename === null || formParam.filename === undefined) {
2979
+ formParam.filename = formParam.contentFile;
2980
+ }
2981
+ if (formParam.contentType === null ||
2982
+ formParam.contentType === undefined) ;
2983
+ }
2984
+ else if (!isString && value.charAt(0) === "<") {
2985
+ getParamPart(formParam, value, 1, {
2986
+ encoder: true,
2987
+ headers: true,
2988
+ }, warnings);
2989
+ formParam.contentFile = formParam.content;
2990
+ delete formParam.content;
2991
+ if (formParam.contentType === null ||
2992
+ formParam.contentType === undefined) ;
2993
+ }
2994
+ else {
2995
+ if (isString) {
2996
+ formParam.content = value;
2997
+ }
2998
+ else {
2999
+ getParamPart(formParam, value, 0, {
3000
+ filename: true,
3001
+ encoder: true,
3002
+ headers: true,
3003
+ }, warnings);
3004
+ }
3005
+ }
3006
+ multipartUploads.push(formParam);
3007
+ }
3008
+ return multipartUploads;
3009
+ }
3010
+
3011
+ function buildURL(global, config, url, uploadFile, outputFile, stdin, stdinFile) {
3012
+ const originalUrl = url;
3013
+ const u = parseurl(global, config, url);
3014
+ // https://github.com/curl/curl/blob/curl-7_85_0/src/tool_operate.c#L1124
3015
+ // https://github.com/curl/curl/blob/curl-7_85_0/src/tool_operhlp.c#L76
3016
+ if (uploadFile) {
3017
+ // TODO: it's more complicated
3018
+ if (u.path.isEmpty()) {
3019
+ u.path = uploadFile.prepend("/");
3020
+ }
3021
+ else if (u.path.endsWith("/")) {
3022
+ u.path = u.path.add(uploadFile);
3023
+ }
3024
+ if (config.get) {
3025
+ warnf(global, [
3026
+ "data-ignored",
3027
+ "curl doesn't let you pass --get and --upload-file together",
3028
+ ]);
3029
+ }
3030
+ }
3031
+ const urlWithOriginalQuery = mergeWords(u.scheme, "://", u.host, u.path, u.query, u.fragment);
3032
+ // curl example.com example.com?foo=bar --url-query isshared=t
3033
+ // will make requests for
3034
+ // example.com/?isshared=t
3035
+ // example.com/?foo=bar&isshared=t
3036
+ //
3037
+ // so the query could come from
3038
+ // 1. `--url` (i.e. the requested URL)
3039
+ // 2. `--url-query` or `--get --data` (the latter takes precedence)
3040
+ //
3041
+ // If it comes from the latter, we might need to generate code to read
3042
+ // from one or more files.
3043
+ // When there's multiple urls, the latter applies to all of them
3044
+ // but the query from --url only applies to that URL.
3045
+ //
3046
+ // There's 3 cases for the query:
3047
+ // 1. it's well-formed and can be expressed as a list of tuples (or a dict)
3048
+ // `?one=1&one=1&two=2`
3049
+ // 2. it can't, for example because one of the pieces doesn't have a '='
3050
+ // `?one`
3051
+ // 3. we need to generate code that reads from a file
3052
+ //
3053
+ // If there's only one URL we merge the query from the URL with the shared part.
3054
+ //
3055
+ // If there's multiple URLs and a shared part that reads from a file (case 3),
3056
+ // we only write the file reading code once, pass it as the params= argument
3057
+ // and the part from the URL has to be passed as a string in the URL
3058
+ // and requests will combine the query in the URL with the query in params=.
3059
+ //
3060
+ // Otherwise, we print each query for each URL individually, either as a
3061
+ // list of tuples if we can or in the URL if we can't.
3062
+ //
3063
+ // When files are passed in through --data-urlencode or --url-query
3064
+ // we can usually treat them as case 1 as well (in Python), but that would
3065
+ // generate code slightly different from curl because curl reads the file once
3066
+ // upfront, whereas we would read the file multiple times and it might contain
3067
+ // different data each time (for example if it's /dev/urandom).
3068
+ let urlQueryArray = null;
3069
+ let queryArray = null;
3070
+ let queryStrReadsFile = null;
3071
+ if (u.query.toBool() || (config["url-query"] && config["url-query"].length)) {
3072
+ let queryStr = null;
3073
+ let queryParts = [];
3074
+ if (u.query.toBool()) {
3075
+ // remove the leading '?'
3076
+ queryParts.push(["raw", u.query.slice(1)]);
3077
+ [queryArray, queryStr, queryStrReadsFile] = buildData(queryParts, stdin, stdinFile);
3078
+ urlQueryArray = queryArray;
3079
+ }
3080
+ if (config["url-query"]) {
3081
+ queryParts = queryParts.concat(config["url-query"]);
3082
+ [queryArray, queryStr, queryStrReadsFile] = buildData(queryParts, stdin, stdinFile);
3083
+ }
3084
+ // TODO: check the curl source code
3085
+ // TODO: curl localhost:8888/?
3086
+ // will request /?
3087
+ // but
3088
+ // curl localhost:8888/? --url-query ''
3089
+ // (or --get --data '') will request /
3090
+ u.query = new Word();
3091
+ if (queryStr && queryStr.toBool()) {
3092
+ u.query = queryStr.prepend("?");
3093
+ }
3094
+ }
3095
+ const urlWithoutQueryArray = mergeWords(u.scheme, "://", u.host, u.path, u.fragment);
3096
+ url = mergeWords(u.scheme, "://", u.host, u.path, u.query, u.fragment);
3097
+ let urlWithoutQueryList = url;
3098
+ // TODO: parseQueryString() doesn't accept leading '?'
3099
+ let [queryList, queryDict] = parseQueryString(u.query.toBool() ? u.query.slice(1) : new Word());
3100
+ if (queryList && queryList.length) {
3101
+ // TODO: remove the fragment too?
3102
+ urlWithoutQueryList = mergeWords(u.scheme, "://", u.host, u.path, u.fragment);
3103
+ }
3104
+ else {
3105
+ queryList = null;
3106
+ queryDict = null;
3107
+ }
3108
+ // TODO: --path-as-is
3109
+ // TODO: --request-target
3110
+ // curl expects you to uppercase methods always. If you do -X PoSt, that's what it
3111
+ // will send, but most APIs will helpfully uppercase what you pass in as the method.
3112
+ //
3113
+ // There are many places where curl determines the method, this is the last one:
3114
+ // https://github.com/curl/curl/blob/curl-7_85_0/lib/http.c#L2032
3115
+ let method = new Word("GET");
3116
+ if (config.request &&
3117
+ // Safari adds `-X null` if it can't determine the request type
3118
+ // https://github.com/WebKit/WebKit/blob/f58ef38d48f42f5d7723691cb090823908ff5f9f/Source/WebInspectorUI/UserInterface/Models/Resource.js#L1250
3119
+ !eq(config.request, "null")) {
3120
+ method = config.request;
3121
+ }
3122
+ else if (config.head) {
3123
+ method = new Word("HEAD");
3124
+ }
3125
+ else if (uploadFile && uploadFile.toBool()) {
3126
+ // --upload-file '' doesn't do anything.
3127
+ method = new Word("PUT");
3128
+ }
3129
+ else if (!config.get && (has(config, "data") || has(config, "form"))) {
3130
+ method = new Word("POST");
3131
+ }
3132
+ const requestUrl = {
3133
+ originalUrl,
3134
+ urlWithoutQueryList,
3135
+ url,
3136
+ urlObj: u,
3137
+ urlWithOriginalQuery,
3138
+ urlWithoutQueryArray,
3139
+ method,
3140
+ };
3141
+ if (queryStrReadsFile) {
3142
+ requestUrl.queryReadsFile = queryStrReadsFile;
3143
+ }
3144
+ if (queryList) {
3145
+ requestUrl.queryList = queryList;
3146
+ if (queryDict) {
3147
+ requestUrl.queryDict = queryDict;
3148
+ }
3149
+ }
3150
+ if (queryArray) {
3151
+ requestUrl.queryArray = queryArray;
3152
+ }
3153
+ if (urlQueryArray) {
3154
+ requestUrl.urlQueryArray = urlQueryArray;
3155
+ }
3156
+ if (uploadFile) {
3157
+ if (eq(uploadFile, "-") || eq(uploadFile, ".")) {
3158
+ if (stdinFile) {
3159
+ requestUrl.uploadFile = stdinFile;
3160
+ }
3161
+ else if (stdin) {
3162
+ warnf(global, [
3163
+ "upload-file-with-stdin-content",
3164
+ "--upload-file with stdin content is not supported",
3165
+ ]);
3166
+ requestUrl.uploadFile = uploadFile;
3167
+ // TODO: this is complicated,
3168
+ // --upload-file only applies per-URL so .data needs to become per-URL...
3169
+ // if you pass --data and --upload-file or --get and --upload-file, curl will error
3170
+ // if (config.url && config.url.length === 1) {
3171
+ // config.data = [["raw", stdin]];
3172
+ // } else {
3173
+ // warnf(global, [
3174
+ // "upload-file-with-stdin-content-and-multiple-urls",
3175
+ // "--upload-file with stdin content and multiple URLs is not supported",
3176
+ // ]);
3177
+ // }
3178
+ }
3179
+ else {
3180
+ requestUrl.uploadFile = uploadFile;
3181
+ }
3182
+ }
3183
+ else {
3184
+ requestUrl.uploadFile = uploadFile;
3185
+ }
3186
+ }
3187
+ if (outputFile) {
3188
+ // TODO: get stdout redirects of command
3189
+ requestUrl.output = outputFile;
3190
+ }
3191
+ // --user takes precedence over the URL
3192
+ const auth = config.user || u.auth;
3193
+ if (auth) {
3194
+ const [user, pass] = auth.split(":", 2);
3195
+ requestUrl.auth = [user, pass || new Word()];
3196
+ }
3197
+ return requestUrl;
3198
+ }
3199
+ function buildData(configData, stdin, stdinFile) {
3200
+ const data = [];
3201
+ let dataStrState = new Word();
3202
+ for (const [i, x] of configData.entries()) {
3203
+ const type = x[0];
3204
+ let value = x[1];
3205
+ let name = null;
3206
+ if (i > 0 && type !== "json") {
3207
+ dataStrState = dataStrState.append("&");
3208
+ }
3209
+ if (type === "urlencode") {
3210
+ // curl checks for = before @
3211
+ const splitOn = value.includes("=") || !value.includes("@") ? "=" : "@";
3212
+ // If there's no = or @ then the entire content is treated as a value and encoded
3213
+ if (value.includes("@") || value.includes("=")) {
3214
+ [name, value] = value.split(splitOn, 2);
3215
+ }
3216
+ if (splitOn === "=") {
3217
+ if (name && name.toBool()) {
3218
+ dataStrState = dataStrState.add(name).append("=");
3219
+ }
3220
+ // curl's --data-urlencode percent-encodes spaces as "+"
3221
+ // https://github.com/curl/curl/blob/curl-7_86_0/src/tool_getparam.c#L630
3222
+ dataStrState = dataStrState.add(percentEncodePlus(value));
3223
+ continue;
3224
+ }
3225
+ name = name && name.toBool() ? name : null;
3226
+ value = value.prepend("@");
3227
+ }
3228
+ let filename = null;
3229
+ if (type !== "raw" && value.startsWith("@")) {
3230
+ filename = value.slice(1);
3231
+ if (eq(filename, "-")) {
3232
+ if (stdin !== undefined) {
3233
+ switch (type) {
3234
+ case "binary":
3235
+ case "json":
3236
+ value = stdin;
3237
+ break;
3238
+ case "urlencode":
3239
+ value = mergeWords(name && name.length ? name.append("=") : new Word(), percentEncodePlus(stdin));
3240
+ break;
3241
+ default:
3242
+ value = stdin.replace(/[\n\r]/g, "");
3243
+ }
3244
+ filename = null;
3245
+ }
3246
+ else if (stdinFile !== undefined) {
3247
+ filename = stdinFile;
3248
+ }
3249
+ else ;
3250
+ }
3251
+ }
3252
+ if (filename !== null) {
3253
+ if (dataStrState.toBool()) {
3254
+ data.push(dataStrState);
3255
+ dataStrState = new Word();
3256
+ }
3257
+ const dataParam = {
3258
+ // If `filename` isn't null, then `type` can't be "raw"
3259
+ filetype: type,
3260
+ filename,
3261
+ };
3262
+ if (name) {
3263
+ dataParam.name = name;
3264
+ }
3265
+ data.push(dataParam);
3266
+ }
3267
+ else {
3268
+ dataStrState = dataStrState.add(value);
3269
+ }
3270
+ }
3271
+ if (dataStrState.toBool()) {
3272
+ data.push(dataStrState);
3273
+ }
3274
+ let dataStrReadsFile = null;
3275
+ const dataStr = mergeWords(...data.map((d) => {
3276
+ if (!(d instanceof Word)) {
3277
+ dataStrReadsFile || (dataStrReadsFile = d.filename.toString()); // report first file
3278
+ if (d.name) {
3279
+ return mergeWords(d.name, "=@", d.filename);
3280
+ }
3281
+ return d.filename.prepend("@");
3282
+ }
3283
+ return d;
3284
+ }));
3285
+ return [data, dataStr, dataStrReadsFile];
3286
+ }
3287
+ function buildRequest(global, config, stdin, stdinFile) {
3288
+ var _a, _b;
3289
+ if (!config.url || !config.url.length) {
3290
+ // TODO: better error message (could be parsing fail)
3291
+ throw new CCError("no URL specified!");
3292
+ }
3293
+ const headers = new Headers(config.header);
3294
+ let cookies;
3295
+ const cookieFiles = [];
3296
+ const cookieHeader = headers.get("cookie");
3297
+ if (cookieHeader) {
3298
+ const parsedCookies = parseCookiesStrict(cookieHeader);
3299
+ if (parsedCookies) {
3300
+ cookies = parsedCookies;
3301
+ }
3302
+ }
3303
+ else if (cookieHeader === undefined && config.cookie) {
3304
+ // If there is a Cookie header, --cookies is ignored
3305
+ const cookieStrings = [];
3306
+ for (const c of config.cookie) {
3307
+ // a --cookie without a = character reads from it as a filename
3308
+ if (c.includes("=")) {
3309
+ cookieStrings.push(c);
3310
+ }
3311
+ else {
3312
+ cookieFiles.push(c);
3313
+ }
3314
+ }
3315
+ if (cookieStrings.length) {
3316
+ const cookieString = joinWords(config.cookie, "; ");
3317
+ headers.setIfMissing("Cookie", cookieString);
3318
+ const parsedCookies = parseCookies(cookieString);
3319
+ if (parsedCookies) {
3320
+ cookies = parsedCookies;
3321
+ }
3322
+ }
3323
+ }
3324
+ if (config["user-agent"]) {
3325
+ headers.setIfMissing("User-Agent", config["user-agent"]);
3326
+ }
3327
+ if (config.referer) {
3328
+ // referer can be ";auto" or followed by ";auto", we ignore that.
3329
+ const referer = config.referer.replace(/;auto$/, "");
3330
+ if (referer.length) {
3331
+ headers.setIfMissing("Referer", referer);
3332
+ }
3333
+ }
3334
+ if (config.range) {
3335
+ let range = config.range.prepend("bytes=");
3336
+ if (!range.includes("-")) {
3337
+ range = range.append("-");
3338
+ }
3339
+ headers.setIfMissing("Range", range);
3340
+ }
3341
+ if (config["time-cond"]) {
3342
+ let timecond = config["time-cond"];
3343
+ let header = "If-Modified-Since";
3344
+ switch (timecond.charAt(0)) {
3345
+ case "+":
3346
+ timecond = timecond.slice(1);
3347
+ break;
3348
+ case "-":
3349
+ timecond = timecond.slice(1);
3350
+ header = "If-Unmodified-Since";
3351
+ break;
3352
+ case "=":
3353
+ timecond = timecond.slice(1);
3354
+ header = "Last-Modified";
3355
+ break;
3356
+ }
3357
+ // TODO: parse date
3358
+ headers.setIfMissing(header, timecond);
3359
+ }
3360
+ let data;
3361
+ let dataStr;
3362
+ let dataStrReadsFile;
3363
+ let queryArray;
3364
+ if (config.data && config.data.length) {
3365
+ if (config.get) {
3366
+ // https://github.com/curl/curl/blob/curl-7_85_0/src/tool_operate.c#L721
3367
+ // --get --data will overwrite --url-query, but if there's no --data, for example,
3368
+ // curl --url-query bar --get example.com
3369
+ // it won't
3370
+ // https://daniel.haxx.se/blog/2022/11/10/append-data-to-the-url-query/
3371
+ config["url-query"] = config.data;
3372
+ delete config.data;
3373
+ }
3374
+ else {
3375
+ [data, dataStr, dataStrReadsFile] = buildData(config.data, stdin, stdinFile);
3376
+ }
3377
+ }
3378
+ if (config["url-query"]) {
3379
+ [queryArray] = buildData(config["url-query"], stdin, stdinFile);
3380
+ }
3381
+ const urls = [];
3382
+ const uploadFiles = config["upload-file"] || [];
3383
+ const outputFiles = config.output || [];
3384
+ for (const [i, url] of config.url.entries()) {
3385
+ urls.push(buildURL(global, config, url, uploadFiles[i], outputFiles[i], stdin, stdinFile));
3386
+ }
3387
+ // --get moves --data into the URL's query string
3388
+ if (config.get && config.data) {
3389
+ delete config.data;
3390
+ }
3391
+ if ((config["upload-file"] || []).length > config.url.length) {
3392
+ warnf(global, [
3393
+ "too-many-upload-files",
3394
+ "Got more --upload-file/-T options than URLs: " +
3395
+ ((_a = config["upload-file"]) === null || _a === void 0 ? void 0 : _a.map((f) => JSON.stringify(f.toString())).join(", ")),
3396
+ ]);
3397
+ }
3398
+ if ((config.output || []).length > config.url.length) {
3399
+ warnf(global, [
3400
+ "too-many-ouptut-files",
3401
+ "Got more --output/-o options than URLs: " +
3402
+ ((_b = config.output) === null || _b === void 0 ? void 0 : _b.map((f) => JSON.stringify(f.toString())).join(", ")),
3403
+ ]);
3404
+ }
3405
+ const request = {
3406
+ urls,
3407
+ authType: pickAuth(config.authtype),
3408
+ headers,
3409
+ };
3410
+ // TODO: warn about unused stdin?
3411
+ if (stdin) {
3412
+ request.stdin = stdin;
3413
+ }
3414
+ if (stdinFile) {
3415
+ request.stdinFile = stdinFile;
3416
+ }
3417
+ if (config.globoff !== undefined) {
3418
+ request.globoff = config.globoff;
3419
+ }
3420
+ if (cookies) {
3421
+ // generators that use .cookies need to do
3422
+ // deleteHeader(request, 'cookie')
3423
+ request.cookies = cookies;
3424
+ }
3425
+ if (cookieFiles.length) {
3426
+ request.cookieFiles = cookieFiles;
3427
+ }
3428
+ if (config["cookie-jar"]) {
3429
+ request.cookieJar = config["cookie-jar"];
3430
+ }
3431
+ if (config.compressed !== undefined) {
3432
+ request.compressed = config.compressed;
3433
+ }
3434
+ if (config.json) {
3435
+ headers.setIfMissing("Content-Type", "application/json");
3436
+ headers.setIfMissing("Accept", "application/json");
3437
+ }
3438
+ else if (config.data) {
3439
+ headers.setIfMissing("Content-Type", "application/x-www-form-urlencoded");
3440
+ }
3441
+ else if (config.form) {
3442
+ // TODO: warn when details (;filename=, etc.) are not supported
3443
+ // by each converter.
3444
+ request.multipartUploads = parseForm(config.form, global.warnings);
3445
+ }
3446
+ if (config["aws-sigv4"]) {
3447
+ // https://github.com/curl/curl/blob/curl-7_86_0/lib/setopt.c#L678-L679
3448
+ request.authType = "aws-sigv4";
3449
+ request.awsSigV4 = config["aws-sigv4"];
3450
+ }
3451
+ if (request.authType === "bearer" && config["oauth2-bearer"]) {
3452
+ const bearer = config["oauth2-bearer"].prepend("Bearer ");
3453
+ headers.setIfMissing("Authorization", bearer);
3454
+ }
3455
+ if (config.delegation) {
3456
+ request.delegation = config.delegation;
3457
+ }
3458
+ // TODO: ideally we should generate code that explicitly unsets the header too
3459
+ // no HTTP libraries allow that.
3460
+ headers.clearNulls();
3461
+ if (config.data && config.data.length) {
3462
+ request.data = dataStr;
3463
+ if (dataStrReadsFile) {
3464
+ request.dataReadsFile = dataStrReadsFile;
3465
+ }
3466
+ request.dataArray = data;
3467
+ // TODO: remove these
3468
+ request.isDataRaw = false;
3469
+ request.isDataBinary = (data || []).some((d) => !(d instanceof Word) && d.filetype === "binary");
3470
+ }
3471
+ if (queryArray) {
3472
+ // If we have to generate code that reads from a file, we
3473
+ // need to do it once for all URLs.
3474
+ request.queryArray = queryArray;
3475
+ }
3476
+ if (config["ipv4"] !== undefined) {
3477
+ request["ipv4"] = config["ipv4"];
3478
+ }
3479
+ if (config["ipv6"] !== undefined) {
3480
+ request["ipv6"] = config["ipv6"];
3481
+ }
3482
+ if (config.ciphers) {
3483
+ request.ciphers = config.ciphers;
3484
+ }
3485
+ if (config.insecure) {
3486
+ request.insecure = true;
3487
+ }
3488
+ // TODO: if the URL doesn't start with https://, curl doesn't verify
3489
+ // certificates, etc.
3490
+ if (config.cert) {
3491
+ if (config.cert.startsWith("pkcs11:") || !config.cert.match(/[:\\]/)) {
3492
+ request.cert = [config.cert, null];
3493
+ }
3494
+ else {
3495
+ // TODO: curl does more complex processing
3496
+ // find un-backslash-escaped colon, backslash might also be escaped with a backslash
3497
+ let colon = -1;
3498
+ try {
3499
+ // Safari versions older than 16.4 don't support negative lookbehind
3500
+ colon = config.cert.search(/(?<!\\)(?:\\\\)*:/);
3501
+ }
3502
+ catch (_c) {
3503
+ colon = config.cert.search(/:/);
3504
+ }
3505
+ if (colon === -1) {
3506
+ request.cert = [config.cert, null];
3507
+ }
3508
+ else {
3509
+ const cert = config.cert.slice(0, colon);
3510
+ const password = config.cert.slice(colon + 1);
3511
+ if (password.toBool()) {
3512
+ request.cert = [cert, password];
3513
+ }
3514
+ else {
3515
+ request.cert = [cert, null];
3516
+ }
3517
+ }
3518
+ }
3519
+ }
3520
+ if (config["cert-type"]) {
3521
+ request.certType = config["cert-type"];
3522
+ }
3523
+ if (config.key) {
3524
+ request.key = config.key;
3525
+ }
3526
+ if (config["key-type"]) {
3527
+ request.keyType = config["key-type"];
3528
+ }
3529
+ if (config.cacert) {
3530
+ request.cacert = config.cacert;
3531
+ }
3532
+ if (config.capath) {
3533
+ request.capath = config.capath;
3534
+ }
3535
+ if (config.crlfile) {
3536
+ request.crlfile = config.crlfile;
3537
+ }
3538
+ if (config.pinnedpubkey) {
3539
+ request.pinnedpubkey = config.pinnedpubkey;
3540
+ }
3541
+ if (config["random-file"]) {
3542
+ request.randomFile = config["random-file"];
3543
+ }
3544
+ if (config["egd-file"]) {
3545
+ request.egdFile = config["egd-file"];
3546
+ }
3547
+ if (config.hsts) {
3548
+ request.hsts = config.hsts;
3549
+ }
3550
+ if (config.proxy) {
3551
+ // https://github.com/curl/curl/blob/e498a9b1fe5964a18eb2a3a99dc52160d2768261/lib/url.c#L2388-L2390
3552
+ request.proxy = config.proxy;
3553
+ if (config["proxy-user"]) {
3554
+ request.proxyAuth = config["proxy-user"];
3555
+ }
3556
+ }
3557
+ if (config.noproxy) {
3558
+ request.noproxy = config.noproxy;
3559
+ }
3560
+ if (config["max-time"]) {
3561
+ request.timeout = config["max-time"];
3562
+ if (config["max-time"].isString() &&
3563
+ // TODO: parseFloat() like curl
3564
+ isNaN(parseFloat(config["max-time"].toString()))) {
3565
+ warnf(global, [
3566
+ "max-time-not-number",
3567
+ "option --max-time: expected a proper numerical parameter: " +
3568
+ JSON.stringify(config["max-time"].toString()),
3569
+ ]);
3570
+ }
3571
+ }
3572
+ if (config["connect-timeout"]) {
3573
+ request.connectTimeout = config["connect-timeout"];
3574
+ if (config["connect-timeout"].isString() &&
3575
+ isNaN(parseFloat(config["connect-timeout"].toString()))) {
3576
+ warnf(global, [
3577
+ "connect-timeout-not-number",
3578
+ "option --connect-timeout: expected a proper numerical parameter: " +
3579
+ JSON.stringify(config["connect-timeout"].toString()),
3580
+ ]);
3581
+ }
3582
+ }
3583
+ if (config["limit-rate"]) {
3584
+ request.limitRate = config["limit-rate"];
3585
+ }
3586
+ if (Object.prototype.hasOwnProperty.call(config, "keepalive")) {
3587
+ request.keepAlive = config.keepalive;
3588
+ }
3589
+ if (Object.prototype.hasOwnProperty.call(config, "location")) {
3590
+ request.followRedirects = config.location;
3591
+ }
3592
+ if (config["location-trusted"]) {
3593
+ request.followRedirectsTrusted = config["location-trusted"];
3594
+ }
3595
+ if (config["max-redirs"]) {
3596
+ request.maxRedirects = config["max-redirs"].trim();
3597
+ if (config["max-redirs"].isString() &&
3598
+ !isInt(config["max-redirs"].toString())) {
3599
+ warnf(global, [
3600
+ "max-redirs-not-int",
3601
+ "option --max-redirs: expected a proper numerical parameter: " +
3602
+ JSON.stringify(config["max-redirs"].toString()),
3603
+ ]);
3604
+ }
3605
+ }
3606
+ if (config.retry) {
3607
+ request.retry = config.retry;
3608
+ }
3609
+ // TODO: this should write to the same "httpVersion" variable
3610
+ const http2 = config.http2 || config["http2-prior-knowledge"];
3611
+ if (http2) {
3612
+ request.http2 = http2;
3613
+ }
3614
+ if (config.http3 || config["http3-only"]) {
3615
+ request.http3 = true;
3616
+ }
3617
+ if (config["unix-socket"]) {
3618
+ request.unixSocket = config["unix-socket"];
3619
+ }
3620
+ if (config["netrc-optional"] || config["netrc-file"]) {
3621
+ request.netrc = "optional";
3622
+ }
3623
+ else if (config.netrc) {
3624
+ request.netrc = "required";
3625
+ }
3626
+ else if (config.netrc === false) {
3627
+ // TODO || config["netrc-optional"] === false ?
3628
+ request.netrc = "ignored";
3629
+ }
3630
+ if (config["continue-at"]) {
3631
+ request.continueAt = config["continue-at"];
3632
+ }
3633
+ if (Object.prototype.hasOwnProperty.call(config, "clobber")) {
3634
+ request.clobber = config.clobber;
3635
+ }
3636
+ if (Object.prototype.hasOwnProperty.call(config, "remote-time")) {
3637
+ request.remoteTime = config["remote-time"];
3638
+ }
3639
+ // Global options
3640
+ if (Object.prototype.hasOwnProperty.call(global, "verbose")) {
3641
+ request.verbose = global.verbose;
3642
+ }
3643
+ if (Object.prototype.hasOwnProperty.call(global, "silent")) {
3644
+ request.silent = global.silent;
3645
+ }
3646
+ return request;
3647
+ }
3648
+ function buildRequests(global, stdin, stdinFile) {
3649
+ if (!global.configs.length) {
3650
+ // shouldn't happen
3651
+ warnf(global, ["no-configs", "got empty config object"]);
3652
+ }
3653
+ return global.configs.map((config) => buildRequest(global, config, stdin, stdinFile));
3654
+ }
3655
+ function getFirst(requests, warnings, support) {
3656
+ if (requests.length > 1) {
3657
+ warnings.push([
3658
+ "next",
3659
+ // TODO: better message, we might have two requests because of
3660
+ // --next or because of multiple curl commands or both
3661
+ "got " +
3662
+ requests.length +
3663
+ " curl requests, only converting the first one",
3664
+ ]);
3665
+ }
3666
+ const request = requests[0];
3667
+ warnIfPartsIgnored(request, warnings, support);
3668
+ return request;
3669
+ }
3670
+
3671
+ function clip(s, maxLength = 30) {
3672
+ if (s.length > maxLength) {
3673
+ return s.slice(0, maxLength - 3) + "...";
3674
+ }
3675
+ return s;
3676
+ }
3677
+ function findCommands(curlCommand, warnings) {
3678
+ if (typeof curlCommand === "string") {
3679
+ return tokenize(curlCommand, warnings);
3680
+ }
3681
+ if (curlCommand.length === 0) {
3682
+ throw new CCError("no arguments provided");
3683
+ }
3684
+ if (curlCommand[0].trim() !== "curl") {
3685
+ throw new CCError('command should begin with "curl" but instead begins with ' +
3686
+ JSON.stringify(clip(curlCommand[0])));
3687
+ }
3688
+ return [[curlCommand.map((arg) => new Word(arg)), undefined, undefined]];
3689
+ }
3690
+ /**
3691
+ * Accepts a string of Bash code or a tokenized argv array.
3692
+ * Returns an array of parsed curl objects.
3693
+ * @param command a string of Bash code containing at least one curl command or an
3694
+ * array of shell argument tokens (meant for passing process.argv).
3695
+ */
3696
+ function parse(command, supportedArgs, warnings = []) {
3697
+ let requests = [];
3698
+ const curlCommands = findCommands(command, warnings);
3699
+ for (const [argv, stdin, stdinFile] of curlCommands) {
3700
+ const globalConfig = parseArgs(argv, curlLongOpts, curlLongOptsShortened, curlShortOpts, supportedArgs, warnings);
3701
+ requests = requests.concat(buildRequests(globalConfig, stdin, stdinFile));
3702
+ }
3703
+ return requests;
3704
+ }
3705
+
3706
+ exports.COMMON_SUPPORTED_ARGS = COMMON_SUPPORTED_ARGS;
3707
+ exports.clip = clip;
3708
+ exports.getFirst = getFirst;
3709
+ exports.parse = parse;