fauxnix-cli 0.9.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -26
- package/dist/ast.d.ts +38 -3
- package/dist/ast.js +22 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +84 -20
- package/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.js +169 -25
- package/dist/commands/install-all.js +4 -2
- package/dist/commands/net.d.ts +9 -0
- package/dist/commands/net.js +51 -15
- package/dist/commands/sysinfo.d.ts +2 -1
- package/dist/commands/sysinfo.js +610 -82
- package/dist/commands/text-filters.d.ts +1 -0
- package/dist/commands/text-filters.js +99 -13
- package/dist/commands/text-io.js +72 -43
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +292 -0
- package/dist/errors.js +14 -4
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +194 -51
- package/dist/install.d.ts +15 -0
- package/dist/install.js +247 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +49 -26
- package/dist/parser.js +432 -35
- package/dist/powershell.d.ts +22 -0
- package/dist/powershell.js +129 -0
- package/dist/ps-host.d.ts +41 -13
- package/dist/ps-host.js +302 -40
- package/dist/qwen-launch.d.ts +12 -0
- package/dist/qwen-launch.js +29 -0
- package/dist/registry.d.ts +22 -0
- package/dist/registry.js +109 -1
- package/dist/translator.d.ts +42 -9
- package/dist/translator.js +697 -95
- package/package.json +3 -1
package/dist/commands/archive.js
CHANGED
|
@@ -28,19 +28,89 @@ const PS_GZ_FNS = [
|
|
|
28
28
|
' $fx_gz.Close()',
|
|
29
29
|
' return ,$fx_ms.ToArray()',
|
|
30
30
|
'}',
|
|
31
|
-
'function fx-gz-
|
|
32
|
-
' $
|
|
33
|
-
' $
|
|
34
|
-
'
|
|
31
|
+
'function fx-gz-open($src, $isBytes) {',
|
|
32
|
+
' if ($isBytes) { $fx_raw = New-Object System.IO.MemoryStream(,$src) }',
|
|
33
|
+
' else { $fx_raw = [IO.File]::OpenRead($src) }',
|
|
34
|
+
' try {',
|
|
35
|
+
' return (New-Object System.IO.Compression.GZipStream($fx_raw, [System.IO.Compression.CompressionMode]::Decompress))',
|
|
36
|
+
' } catch {',
|
|
37
|
+
' $fx_raw.Dispose()',
|
|
38
|
+
' throw',
|
|
39
|
+
' }',
|
|
40
|
+
'}',
|
|
41
|
+
// Drain the complete stream before producing stdout. Besides keeping -t
|
|
42
|
+
// bounded, this preserves the old all-or-nothing behavior for malformed
|
|
43
|
+
// inputs while remembering which text decoder the second pass should use.
|
|
44
|
+
'function fx-gz-validate($src, $isBytes) {',
|
|
45
|
+
' $fx_gz = fx-gz-open $src $isBytes',
|
|
35
46
|
' $fx_buf = New-Object byte[] 65536',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
47
|
+
' $fx_dec = (New-Object System.Text.UTF8Encoding($false, $true)).GetDecoder()',
|
|
48
|
+
' $fx_utf8 = $true',
|
|
49
|
+
' try {',
|
|
50
|
+
' while ($true) {',
|
|
51
|
+
' $fx_n = $fx_gz.Read($fx_buf, 0, $fx_buf.Length)',
|
|
52
|
+
' if ($fx_n -le 0) { break }',
|
|
53
|
+
' if ($fx_utf8) {',
|
|
54
|
+
' try { [void]$fx_dec.GetCharCount($fx_buf, 0, $fx_n, $false) }',
|
|
55
|
+
' catch [System.Text.DecoderFallbackException] { $fx_utf8 = $false }',
|
|
56
|
+
' }',
|
|
57
|
+
' }',
|
|
58
|
+
' if ($fx_utf8) {',
|
|
59
|
+
' try { [void]$fx_dec.GetCharCount($fx_buf, 0, 0, $true) }',
|
|
60
|
+
' catch [System.Text.DecoderFallbackException] { $fx_utf8 = $false }',
|
|
61
|
+
' }',
|
|
62
|
+
' return $fx_utf8',
|
|
63
|
+
' } finally {',
|
|
64
|
+
' $fx_gz.Dispose()',
|
|
65
|
+
' }',
|
|
66
|
+
'}',
|
|
67
|
+
'function fx-gz-stream-text($src, $isBytes) {',
|
|
68
|
+
' $fx_utf8 = fx-gz-validate $src $isBytes',
|
|
69
|
+
' if ($fx_utf8) { $fx_enc = New-Object System.Text.UTF8Encoding($false, $true) }',
|
|
70
|
+
' else { try { $fx_enc = [System.Text.Encoding]::GetEncoding(936) } catch { $fx_enc = [System.Text.Encoding]::ASCII } }',
|
|
71
|
+
' $fx_gz = fx-gz-open $src $isBytes',
|
|
72
|
+
' $fx_sr = New-Object System.IO.StreamReader($fx_gz, $fx_enc, $false, 65536)',
|
|
73
|
+
' try {',
|
|
74
|
+
' while ($true) {',
|
|
75
|
+
' $fx_line = $fx_sr.ReadLine()',
|
|
76
|
+
' if ($null -eq $fx_line) { break }',
|
|
77
|
+
' Write-Output -NoEnumerate $fx_line',
|
|
78
|
+
' }',
|
|
79
|
+
' } finally {',
|
|
80
|
+
' $fx_sr.Dispose()',
|
|
81
|
+
' }',
|
|
82
|
+
'}',
|
|
83
|
+
// File output is streamed into a sibling temporary file. The destination
|
|
84
|
+
// changes only after a complete decompression, and the temporary is removed
|
|
85
|
+
// on every failed path.
|
|
86
|
+
'function fx-gz-stream-file($src, $dst) {',
|
|
87
|
+
' $fx_full = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($dst)',
|
|
88
|
+
' $fx_parent = [IO.Path]::GetDirectoryName($fx_full)',
|
|
89
|
+
" $fx_tmp = [IO.Path]::Combine($fx_parent, '.fauxnix-gzip-' + [Guid]::NewGuid().ToString('N') + '.tmp')",
|
|
90
|
+
' $fx_gz = $null',
|
|
91
|
+
' $fx_os = $null',
|
|
92
|
+
' try {',
|
|
93
|
+
' $fx_gz = fx-gz-open $src $false',
|
|
94
|
+
" $fx_os = New-Object System.IO.FileStream($fx_tmp, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)",
|
|
95
|
+
' $fx_buf = New-Object byte[] 65536',
|
|
96
|
+
' while ($true) {',
|
|
97
|
+
' $fx_n = $fx_gz.Read($fx_buf, 0, $fx_buf.Length)',
|
|
98
|
+
' if ($fx_n -le 0) { break }',
|
|
99
|
+
' $fx_os.Write($fx_buf, 0, $fx_n)',
|
|
100
|
+
' }',
|
|
101
|
+
' $fx_os.Dispose(); $fx_os = $null',
|
|
102
|
+
' $fx_gz.Dispose(); $fx_gz = $null',
|
|
103
|
+
' if ([IO.File]::Exists($fx_full)) {',
|
|
104
|
+
' $fx_null = [System.Management.Automation.Language.NullString]::Value',
|
|
105
|
+
' [IO.File]::Replace($fx_tmp, $fx_full, $fx_null)',
|
|
106
|
+
' }',
|
|
107
|
+
' else { [IO.File]::Move($fx_tmp, $fx_full) }',
|
|
108
|
+
' } catch {',
|
|
109
|
+
' if ($null -ne $fx_os) { $fx_os.Dispose() }',
|
|
110
|
+
' if ($null -ne $fx_gz) { $fx_gz.Dispose() }',
|
|
111
|
+
' if ([IO.File]::Exists($fx_tmp)) { [IO.File]::Delete($fx_tmp) }',
|
|
112
|
+
' throw',
|
|
40
113
|
' }',
|
|
41
|
-
' $fx_gz.Close()',
|
|
42
|
-
' $fx_fs.Close()',
|
|
43
|
-
' return ,$fx_ms.ToArray()',
|
|
44
114
|
'}',
|
|
45
115
|
].join('\n');
|
|
46
116
|
/** Parse GNU gzip-style argv (works for gzip/gunzip/zcat). */
|
|
@@ -121,7 +191,7 @@ function gzBlock(args, ctx, forced) {
|
|
|
121
191
|
: " [Console]::Error.WriteLine('gzip: compressed data not written to a terminal. Use -f to force compression.')",
|
|
122
192
|
' $script:fx_exit = 1',
|
|
123
193
|
].join('\n')
|
|
124
|
-
: p.decompress
|
|
194
|
+
: p.decompress || p.test
|
|
125
195
|
? [
|
|
126
196
|
// NOTE stdin fidelity limitation: pipeline stdin arrives as decoded
|
|
127
197
|
// text lines, so only gzip streams that survived the text pipeline
|
|
@@ -130,13 +200,7 @@ function gzBlock(args, ctx, forced) {
|
|
|
130
200
|
" if ($fx_txt -ne '') { $fx_txt = $fx_txt + \"`n\" }",
|
|
131
201
|
' $fx_in = [System.Text.Encoding]::UTF8.GetBytes($fx_txt)',
|
|
132
202
|
' try {',
|
|
133
|
-
' $
|
|
134
|
-
' $fx_gz = New-Object System.IO.Compression.GZipStream($fx_ms, [System.IO.Compression.CompressionMode]::Decompress)',
|
|
135
|
-
' $fx_mo = New-Object System.IO.MemoryStream',
|
|
136
|
-
' $fx_buf = New-Object byte[] 65536',
|
|
137
|
-
' while ($true) { $fx_n = $fx_gz.Read($fx_buf, 0, $fx_buf.Length); if ($fx_n -le 0) { break }; $fx_mo.Write($fx_buf, 0, $fx_n) }',
|
|
138
|
-
' $fx_gz.Close()',
|
|
139
|
-
' fx-lines (fx-dec $fx_mo.ToArray())',
|
|
203
|
+
p.test ? ' [void](fx-gz-validate $fx_in $true)' : ' fx-gz-stream-text $fx_in $true',
|
|
140
204
|
' } catch {',
|
|
141
205
|
" [Console]::Error.WriteLine('gzip: stdin: not in gzip format')",
|
|
142
206
|
' $script:fx_exit = 1',
|
|
@@ -163,14 +227,13 @@ function gzBlock(args, ctx, forced) {
|
|
|
163
227
|
' }',
|
|
164
228
|
];
|
|
165
229
|
if (p.test) {
|
|
166
|
-
fileLoop.push(' try {
|
|
230
|
+
fileLoop.push(' try { [void](fx-gz-validate $fx_f $false) }', " catch { [Console]::Error.WriteLine('gzip: ' + $fx_f + ': not in gzip format'); $script:fx_exit = 1 }", ' continue');
|
|
167
231
|
}
|
|
168
232
|
else if (p.decompress) {
|
|
169
233
|
fileLoop.push(' $fx_low = $fx_f.ToLower()', " if (-not ($fx_low.EndsWith('.gz') -or $fx_low.EndsWith('.tgz'))) {", " [Console]::Error.WriteLine('gzip: ' + $fx_f + ': unknown suffix -- ignored')", ' $script:fx_exit = 2', ' continue', ' }', ' $fx_out = $fx_f.Substring(0, $fx_f.Length - 3)', " if ($fx_low.EndsWith('.tgz')) { $fx_out = $fx_f.Substring(0, $fx_f.Length - 4) + '.tar' }", ' try {', p.stdout
|
|
170
|
-
? ' fx-
|
|
234
|
+
? ' fx-gz-stream-text $fx_f $false'
|
|
171
235
|
: [
|
|
172
|
-
'
|
|
173
|
-
' [IO.File]::WriteAllBytes($fx_out, $fx_o)',
|
|
236
|
+
' fx-gz-stream-file $fx_f $fx_out',
|
|
174
237
|
' try { (Get-Item -LiteralPath $fx_out).LastWriteTime = (Get-Item -LiteralPath $fx_f).LastWriteTime } catch {}',
|
|
175
238
|
' if (-not ' + b(p.keep) + ') { Remove-Item -LiteralPath $fx_f -Force }',
|
|
176
239
|
].join('\n'), " } catch { [Console]::Error.WriteLine('gzip: ' + $fx_f + ': not in gzip format'); $script:fx_exit = 1 }");
|
|
@@ -201,6 +264,8 @@ const gunzip = (args, ctx) => gzBlock(args, ctx, { decompress: true });
|
|
|
201
264
|
const zcat = (args, ctx) => gzBlock(args, ctx, { decompress: true, stdout: true });
|
|
202
265
|
/* ------------------------------------------------------------------ */
|
|
203
266
|
/* tar — Windows 10+ ships bsdtar as tar.exe, pass everything through */
|
|
267
|
+
/* Intentionally unspec'd: registerSpec fail-loud would reject GNU */
|
|
268
|
+
/* flags bsdtar accepts (--numeric-owner, …). Same class as find. */
|
|
204
269
|
/* ------------------------------------------------------------------ */
|
|
205
270
|
const tar = (args) => {
|
|
206
271
|
return [
|
|
@@ -276,7 +341,7 @@ const zip = (args) => {
|
|
|
276
341
|
" $fx_dst = $fx_arc",
|
|
277
342
|
' $fx_rename = $false',
|
|
278
343
|
" if (-not $fx_arc.ToLower().EndsWith('.zip')) { $fx_dst = $fx_arc + '.zip'; $fx_rename = $true }",
|
|
279
|
-
' Compress-Archive -
|
|
344
|
+
' Compress-Archive -LiteralPath $fx_valid -DestinationPath $fx_dst -Force -ErrorAction Stop',
|
|
280
345
|
// PS 5.1 Compress-Archive writes `\` entry separators; rewrite them to `/`
|
|
281
346
|
// so GNU unzip/bsdtar see proper directory entries.
|
|
282
347
|
' Add-Type -AssemblyName System.IO.Compression.FileSystem',
|
|
@@ -374,8 +439,87 @@ const unzip = (args) => {
|
|
|
374
439
|
].join('\n');
|
|
375
440
|
};
|
|
376
441
|
/* ------------------------------------------------------------------ */
|
|
377
|
-
/*
|
|
442
|
+
/* CommandSpec (C-5 archive slice) */
|
|
443
|
+
/* tar stays unspec'd: fx-native to tar.exe; unknown GNU flags must */
|
|
444
|
+
/* reach bsdtar. gzip -f is a no-op today and the stdin-from-terminal */
|
|
445
|
+
/* message advertises it, so it is unsupported (force from terminal) */
|
|
446
|
+
/* rather than a silent ignore. */
|
|
378
447
|
/* ------------------------------------------------------------------ */
|
|
448
|
+
const gzipOptions = [
|
|
449
|
+
{ short: 'd', long: '--decompress', support: 'implemented' },
|
|
450
|
+
{ long: '--uncompress', support: 'implemented' },
|
|
451
|
+
{ short: 'k', long: '--keep', support: 'implemented' },
|
|
452
|
+
{ short: 'c', long: '--stdout', support: 'implemented' },
|
|
453
|
+
{ long: '--to-stdout', support: 'implemented' },
|
|
454
|
+
{ short: 't', long: '--test', support: 'implemented' },
|
|
455
|
+
{ short: '1', long: '--fast', support: 'implemented' },
|
|
456
|
+
{ short: '2', support: 'implemented' },
|
|
457
|
+
{ short: '3', support: 'implemented' },
|
|
458
|
+
{ short: '4', support: 'implemented' },
|
|
459
|
+
{ short: '5', support: 'implemented' },
|
|
460
|
+
{ short: '6', support: 'implemented' },
|
|
461
|
+
{ short: '7', support: 'implemented' },
|
|
462
|
+
{ short: '8', support: 'implemented' },
|
|
463
|
+
{ short: '9', long: '--best', support: 'implemented' },
|
|
464
|
+
{ short: 'f', long: '--force', support: 'unsupported', reason: 'force from terminal' },
|
|
465
|
+
{ short: 'q', long: '--quiet', support: 'unsupported', reason: 'quiet' },
|
|
466
|
+
{ short: 'v', long: '--verbose', support: 'unsupported', reason: 'verbose' },
|
|
467
|
+
{ short: 'n', long: '--no-name', support: 'unsupported', reason: 'no-name' },
|
|
468
|
+
{ short: 'r', long: '--recursive', support: 'unsupported', reason: 'recursive' },
|
|
469
|
+
{ short: 'S', long: '--suffix', takesValue: true, support: 'unsupported', reason: 'suffix' },
|
|
470
|
+
];
|
|
471
|
+
export const specs = [
|
|
472
|
+
{
|
|
473
|
+
names: ['gzip'],
|
|
474
|
+
options: gzipOptions,
|
|
475
|
+
effects: ['read', 'write', 'delete'],
|
|
476
|
+
platform: 'windows-ps51',
|
|
477
|
+
dispatch: 'translated',
|
|
478
|
+
handler: gzip,
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
names: ['gunzip'],
|
|
482
|
+
options: gzipOptions,
|
|
483
|
+
effects: ['read', 'write', 'delete'],
|
|
484
|
+
platform: 'windows-ps51',
|
|
485
|
+
dispatch: 'translated',
|
|
486
|
+
handler: gunzip,
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
names: ['zcat'],
|
|
490
|
+
options: gzipOptions,
|
|
491
|
+
effects: ['read'],
|
|
492
|
+
platform: 'windows-ps51',
|
|
493
|
+
dispatch: 'translated',
|
|
494
|
+
handler: zcat,
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
names: ['zip'],
|
|
498
|
+
options: [
|
|
499
|
+
// Compress-Archive always recurses directory inputs; -q is already quiet.
|
|
500
|
+
{ short: 'r', support: 'implemented' },
|
|
501
|
+
{ short: 'q', support: 'implemented' },
|
|
502
|
+
{ short: 'x', long: '--exclude', takesValue: true, support: 'unsupported', reason: 'exclude patterns' },
|
|
503
|
+
],
|
|
504
|
+
effects: ['read', 'write'],
|
|
505
|
+
platform: 'windows-ps51',
|
|
506
|
+
dispatch: 'translated',
|
|
507
|
+
handler: zip,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
names: ['unzip'],
|
|
511
|
+
options: [
|
|
512
|
+
{ short: 'l', support: 'implemented' },
|
|
513
|
+
{ short: 'o', support: 'implemented' },
|
|
514
|
+
{ short: 'q', support: 'implemented' },
|
|
515
|
+
{ short: 'd', long: '--directory', takesValue: true, support: 'implemented' },
|
|
516
|
+
],
|
|
517
|
+
effects: ['read', 'write'],
|
|
518
|
+
platform: 'windows-ps51',
|
|
519
|
+
dispatch: 'translated',
|
|
520
|
+
handler: unzip,
|
|
521
|
+
},
|
|
522
|
+
];
|
|
379
523
|
export const handlers = {
|
|
380
524
|
tar,
|
|
381
525
|
gzip,
|
|
@@ -2,9 +2,9 @@ import { registerAll, registerSpecs } from '../registry.js';
|
|
|
2
2
|
import { handlers as files, specs as fileSpecs } from './files.js';
|
|
3
3
|
import { handlers as textFilters, specs as textFilterSpecs } from './text-filters.js';
|
|
4
4
|
import { handlers as textIo, specs as textIoSpecs } from './text-io.js';
|
|
5
|
-
import { handlers as sysinfo } from './sysinfo.js';
|
|
5
|
+
import { handlers as sysinfo, specs as sysinfoSpecs } from './sysinfo.js';
|
|
6
6
|
import { handlers as net } from './net.js';
|
|
7
|
-
import { handlers as archive } from './archive.js';
|
|
7
|
+
import { handlers as archive, specs as archiveSpecs } from './archive.js';
|
|
8
8
|
/** Register every built-in Linux command translator. Side-effectful import. */
|
|
9
9
|
export function installAll() {
|
|
10
10
|
registerAll(files);
|
|
@@ -16,5 +16,7 @@ export function installAll() {
|
|
|
16
16
|
registerSpecs(fileSpecs);
|
|
17
17
|
registerSpecs(textIoSpecs);
|
|
18
18
|
registerSpecs(textFilterSpecs);
|
|
19
|
+
registerSpecs(archiveSpecs);
|
|
20
|
+
registerSpecs(sysinfoSpecs);
|
|
19
21
|
}
|
|
20
22
|
installAll();
|
package/dist/commands/net.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import { Handler } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* GNU wget-style default output basename for the curl fallback.
|
|
4
|
+
*
|
|
5
|
+
* A valid UTF-8 percent sequence is decoded exactly once. Malformed sequences
|
|
6
|
+
* remain literal, while characters Windows cannot put in a filename are
|
|
7
|
+
* percent-encoded again. This keeps encoded separators inside one basename
|
|
8
|
+
* instead of accidentally turning them into local path components.
|
|
9
|
+
*/
|
|
10
|
+
export declare function urlFileName(u: string): string;
|
|
2
11
|
export declare const handlers: Record<string, Handler>;
|
package/dist/commands/net.js
CHANGED
|
@@ -105,21 +105,57 @@ const curl = (args) => {
|
|
|
105
105
|
/* ------------------------------------------------------------------ */
|
|
106
106
|
/* wget */
|
|
107
107
|
/* ------------------------------------------------------------------ */
|
|
108
|
-
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
109
|
+
function percentEncodeChar(char) {
|
|
110
|
+
return Array.from(Buffer.from(char, 'utf8'), (byte) => '%' + byte.toString(16).toUpperCase()).join('');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* GNU wget-style default output basename for the curl fallback.
|
|
114
|
+
*
|
|
115
|
+
* A valid UTF-8 percent sequence is decoded exactly once. Malformed sequences
|
|
116
|
+
* remain literal, while characters Windows cannot put in a filename are
|
|
117
|
+
* percent-encoded again. This keeps encoded separators inside one basename
|
|
118
|
+
* instead of accidentally turning them into local path components.
|
|
119
|
+
*/
|
|
120
|
+
export function urlFileName(u) {
|
|
121
|
+
let pathname;
|
|
122
|
+
try {
|
|
123
|
+
pathname = new URL(u).pathname;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return 'index.html';
|
|
127
|
+
}
|
|
128
|
+
// URL normalisation handles dot segments. A bare path or trailing slash uses
|
|
129
|
+
// wget's conventional index filename.
|
|
130
|
+
if (pathname === '' || pathname.endsWith('/'))
|
|
131
|
+
return 'index.html';
|
|
132
|
+
const rawName = pathname.slice(pathname.lastIndexOf('/') + 1);
|
|
133
|
+
let decoded = rawName;
|
|
134
|
+
try {
|
|
135
|
+
decoded = decodeURIComponent(rawName);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Keep malformed percent escapes literal so naming remains deterministic.
|
|
139
|
+
}
|
|
140
|
+
let safe = '';
|
|
141
|
+
for (const char of decoded) {
|
|
142
|
+
const code = char.codePointAt(0) ?? 0;
|
|
143
|
+
if (code <= 0x1f || code === 0x7f || /[<>:"/\\|?*]/.test(char)) {
|
|
144
|
+
safe += percentEncodeChar(char);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
safe += char;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Windows strips trailing dots/spaces and treats DOS device basenames as
|
|
151
|
+
// special even when an extension is present. Preserve the intended text in
|
|
152
|
+
// a stable, ordinary filename instead.
|
|
153
|
+
safe = safe.replace(/[. ]+$/, (suffix) => Array.from(suffix, (char) => percentEncodeChar(char)).join(''));
|
|
154
|
+
if (safe === '' || safe === '.' || safe === '..')
|
|
155
|
+
return 'index.html';
|
|
156
|
+
if (WINDOWS_DEVICE_NAME.test(safe))
|
|
157
|
+
safe = '_' + safe;
|
|
158
|
+
return safe;
|
|
123
159
|
}
|
|
124
160
|
/** Map GNU wget argv → curl.exe argv (only literal words are rewritten). */
|
|
125
161
|
function mapWgetArgs(args) {
|