fauxnix-cli 0.1.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.
@@ -0,0 +1,385 @@
1
+ import { wordToString } from '../ast.js';
2
+ import { psStr } from '../registry.js';
3
+ import { exprOfWord, operandExpr } from '../translator.js';
4
+ /* ------------------------------------------------------------------ */
5
+ /* gzip family — .NET GZipStream helpers */
6
+ /* ------------------------------------------------------------------ */
7
+ const PS_GZ_FNS = [
8
+ 'Add-Type -AssemblyName System.IO.Compression',
9
+ 'function fx-dec($b) {',
10
+ ' try { return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) }',
11
+ ' catch { try { return [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { return [System.Text.Encoding]::ASCII.GetString($b) } }',
12
+ '}',
13
+ // emit text as one string per line (trailing newline does not add an empty line)
14
+ 'function fx-lines($t) {',
15
+ ' if ($null -eq $t) { return @() }',
16
+ ' $fx_ls = @($t -split "`r?`n")',
17
+ " if ($fx_ls.Count -eq 1 -and $fx_ls[0] -eq '') { return @() }",
18
+ " if ($fx_ls[$fx_ls.Count - 1] -eq '') { $fx_ls = @($fx_ls[0..($fx_ls.Count - 2)]) }",
19
+ ' return $fx_ls',
20
+ '}',
21
+ // compress byte[] → byte[]; -1..-3 ≈ Fastest, -4..-9 ≈ Optimal (.NET has no finer steps)
22
+ 'function fx-gz-cbytes($b, $lvl) {',
23
+ ' $fx_lv = [System.IO.Compression.CompressionLevel]::Optimal',
24
+ ' if ($lvl -le 3) { $fx_lv = [System.IO.Compression.CompressionLevel]::Fastest }',
25
+ ' $fx_ms = New-Object System.IO.MemoryStream',
26
+ ' $fx_gz = New-Object System.IO.Compression.GZipStream($fx_ms, $fx_lv, $false)',
27
+ ' $fx_gz.Write($b, 0, $b.Length)',
28
+ ' $fx_gz.Close()',
29
+ ' return ,$fx_ms.ToArray()',
30
+ '}',
31
+ 'function fx-gz-dbytes($src) {',
32
+ ' $fx_fs = [IO.File]::OpenRead($src)',
33
+ ' $fx_gz = New-Object System.IO.Compression.GZipStream($fx_fs, [System.IO.Compression.CompressionMode]::Decompress)',
34
+ ' $fx_ms = New-Object System.IO.MemoryStream',
35
+ ' $fx_buf = New-Object byte[] 65536',
36
+ ' while ($true) {',
37
+ ' $fx_n = $fx_gz.Read($fx_buf, 0, $fx_buf.Length)',
38
+ ' if ($fx_n -le 0) { break }',
39
+ ' $fx_ms.Write($fx_buf, 0, $fx_n)',
40
+ ' }',
41
+ ' $fx_gz.Close()',
42
+ ' $fx_fs.Close()',
43
+ ' return ,$fx_ms.ToArray()',
44
+ '}',
45
+ ].join('\n');
46
+ /** Parse GNU gzip-style argv (works for gzip/gunzip/zcat). */
47
+ function parseGzipArgs(args) {
48
+ const p = {
49
+ decompress: false,
50
+ keep: false,
51
+ stdout: false,
52
+ level: 6,
53
+ test: false,
54
+ files: [],
55
+ };
56
+ const raw = args.map(wordToString);
57
+ let i = 0;
58
+ for (; i < raw.length; i++) {
59
+ const t = raw[i];
60
+ if (t === '--') {
61
+ p.files.push(...args.slice(i + 1));
62
+ return p;
63
+ }
64
+ if (t.startsWith('--')) {
65
+ const eq = t.indexOf('=');
66
+ const name = eq >= 0 ? t.slice(2, eq) : t.slice(2);
67
+ if (name === 'decompress' || name === 'uncompress')
68
+ p.decompress = true;
69
+ else if (name === 'keep')
70
+ p.keep = true;
71
+ else if (name === 'stdout' || name === 'to-stdout')
72
+ p.stdout = true;
73
+ else if (name === 'fast')
74
+ p.level = 1;
75
+ else if (name === 'best')
76
+ p.level = 9;
77
+ else if (name === 'test')
78
+ p.test = true;
79
+ else if (name === 'suffix') {
80
+ if (eq < 0)
81
+ i++; // consumes a following value; unsupported either way
82
+ }
83
+ // other long options (force/quiet/verbose/no-name/recursive...) ignored
84
+ continue;
85
+ }
86
+ if (/^-[1-9]$/.test(t)) {
87
+ p.level = parseInt(t.slice(1), 10);
88
+ continue;
89
+ }
90
+ if (t.startsWith('-') && t.length > 1 && !/^-\d/.test(t)) {
91
+ for (const c of t.slice(1)) {
92
+ if (c === 'd')
93
+ p.decompress = true;
94
+ else if (c === 'k')
95
+ p.keep = true;
96
+ else if (c === 'c')
97
+ p.stdout = true;
98
+ else if (c === 't')
99
+ p.test = true;
100
+ // f/q/v/n/r accepted as no-ops
101
+ }
102
+ continue;
103
+ }
104
+ p.files.push(args[i]);
105
+ }
106
+ return p;
107
+ }
108
+ /** Build the PS block shared by gzip / gunzip / zcat. */
109
+ function gzBlock(args, ctx, forced) {
110
+ const p = { ...parseGzipArgs(args), ...forced };
111
+ // Compressed stdout is binary; the fauxnix contract only carries text
112
+ // lines (and the executor's capture layer is UTF-8 text anyway), so the
113
+ // stream is emitted as one latin-1-decoded string — byte fidelity is a
114
+ // documented limitation (spec: "binary fidelity limitation acceptable").
115
+ const emitBin = (bytesExpr) => '[System.Text.Encoding]::GetEncoding(28591).GetString(' + bytesExpr + ')';
116
+ const b = (v) => (v ? '$true' : '$false');
117
+ const stdinBranch = !ctx.hasStdin
118
+ ? [
119
+ p.decompress || p.test
120
+ ? " [Console]::Error.WriteLine('gzip: compressed data not read from a terminal. Use -f to force decompression.')"
121
+ : " [Console]::Error.WriteLine('gzip: compressed data not written to a terminal. Use -f to force compression.')",
122
+ ' $script:fx_exit = 1',
123
+ ].join('\n')
124
+ : p.decompress
125
+ ? [
126
+ // NOTE stdin fidelity limitation: pipeline stdin arrives as decoded
127
+ // text lines, so only gzip streams that survived the text pipeline
128
+ // round-trip can be decompressed here.
129
+ ' $fx_txt = (@($input | ForEach-Object { [string]$_ }) -join "`n")',
130
+ " if ($fx_txt -ne '') { $fx_txt = $fx_txt + \"`n\" }",
131
+ ' $fx_in = [System.Text.Encoding]::UTF8.GetBytes($fx_txt)',
132
+ ' try {',
133
+ ' $fx_ms = New-Object System.IO.MemoryStream(,$fx_in)',
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())',
140
+ ' } catch {',
141
+ " [Console]::Error.WriteLine('gzip: stdin: not in gzip format')",
142
+ ' $script:fx_exit = 1',
143
+ ' }',
144
+ ].join('\n')
145
+ : [
146
+ // NOTE stdin fidelity limitation: bytes are UTF-8 re-encoded text.
147
+ ' $fx_txt = (@($input | ForEach-Object { [string]$_ }) -join "`n")',
148
+ " if ($fx_txt -ne '') { $fx_txt = $fx_txt + \"`n\" }",
149
+ ' $fx_in = [System.Text.Encoding]::UTF8.GetBytes($fx_txt)',
150
+ ' ' + emitBin('(fx-gz-cbytes $fx_in ' + p.level + ')'),
151
+ ].join('\n');
152
+ const fileLoop = [
153
+ ' foreach ($fx_f in $fx_files) {',
154
+ ' if (-not (Test-Path -LiteralPath $fx_f)) {',
155
+ " [Console]::Error.WriteLine('gzip: ' + $fx_f + ': No such file or directory')",
156
+ ' $script:fx_exit = 1',
157
+ ' continue',
158
+ ' }',
159
+ ' if (Test-Path -LiteralPath $fx_f -PathType Container) {',
160
+ " [Console]::Error.WriteLine('gzip: ' + $fx_f + ': is a directory -- ignored')",
161
+ ' $script:fx_exit = 1',
162
+ ' continue',
163
+ ' }',
164
+ ];
165
+ if (p.test) {
166
+ fileLoop.push(' try { $fx_tb = fx-gz-dbytes $fx_f }', " catch { [Console]::Error.WriteLine('gzip: ' + $fx_f + ': not in gzip format'); $script:fx_exit = 1 }", ' continue');
167
+ }
168
+ else if (p.decompress) {
169
+ 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-lines (fx-dec (fx-gz-dbytes $fx_f))'
171
+ : [
172
+ ' $fx_o = fx-gz-dbytes $fx_f',
173
+ ' [IO.File]::WriteAllBytes($fx_out, $fx_o)',
174
+ ' try { (Get-Item -LiteralPath $fx_out).LastWriteTime = (Get-Item -LiteralPath $fx_f).LastWriteTime } catch {}',
175
+ ' if (-not ' + b(p.keep) + ') { Remove-Item -LiteralPath $fx_f -Force }',
176
+ ].join('\n'), " } catch { [Console]::Error.WriteLine('gzip: ' + $fx_f + ': not in gzip format'); $script:fx_exit = 1 }");
177
+ }
178
+ else {
179
+ fileLoop.push(' $fx_low = $fx_f.ToLower()', " if ($fx_low.EndsWith('.gz') -or $fx_low.EndsWith('.tgz')) {", " [Console]::Error.WriteLine('gzip: ' + $fx_f + ': already has .gz suffix -- unchanged')", ' continue', ' }', ' try {', p.stdout
180
+ ? ' ' + emitBin('(fx-gz-cbytes ([IO.File]::ReadAllBytes($fx_f)) ' + p.level + ')')
181
+ : [
182
+ ' $fx_o = fx-gz-cbytes ([IO.File]::ReadAllBytes($fx_f)) ' + p.level,
183
+ " [IO.File]::WriteAllBytes($fx_f + '.gz', $fx_o)",
184
+ " try { (Get-Item -LiteralPath ($fx_f + '.gz')).LastWriteTime = (Get-Item -LiteralPath $fx_f).LastWriteTime } catch {}",
185
+ ' if (-not ' + b(p.keep) + ') { Remove-Item -LiteralPath $fx_f -Force }',
186
+ ].join('\n'), " } catch { [Console]::Error.WriteLine('gzip: ' + $fx_f + ': ' + $_.Exception.Message); $script:fx_exit = 1 }");
187
+ }
188
+ fileLoop.push(' }');
189
+ return [
190
+ PS_GZ_FNS,
191
+ '$fx_files = @(' + (p.files.length ? p.files.map(operandExpr).join(', ') : '') + ')',
192
+ 'if ($fx_files.Count -eq 0) {',
193
+ stdinBranch,
194
+ '} else {',
195
+ ...fileLoop,
196
+ '}',
197
+ ].join('\n');
198
+ }
199
+ const gzip = (args, ctx) => gzBlock(args, ctx, {});
200
+ const gunzip = (args, ctx) => gzBlock(args, ctx, { decompress: true });
201
+ const zcat = (args, ctx) => gzBlock(args, ctx, { decompress: true, stdout: true });
202
+ /* ------------------------------------------------------------------ */
203
+ /* tar — Windows 10+ ships bsdtar as tar.exe, pass everything through */
204
+ /* ------------------------------------------------------------------ */
205
+ const tar = (args) => {
206
+ return [
207
+ "$fx_args = @(" + args.map(exprOfWord).join(', ') + ')',
208
+ // Prefer the Windows-shipped bsdtar (System32): it accepts both path
209
+ // styles. A PATH lookup could resolve to Git Bash's GNU tar, which
210
+ // misreads `C:\...` argv as a remote-host spec (host:path syntax).
211
+ '$fx_tar = "$env:SystemRoot\\System32\\tar.exe"',
212
+ 'if (-not (Test-Path -LiteralPath $fx_tar)) {',
213
+ " $fx_c = Get-Command 'tar.exe' -ErrorAction SilentlyContinue",
214
+ ' if ($fx_c) { $fx_tar = $fx_c.Source } else { $fx_tar = $null }',
215
+ '}',
216
+ 'if ($fx_tar) {',
217
+ ' & $fx_tar @($fx_args) | ForEach-Object { [string]$_ }',
218
+ ' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE }',
219
+ '} else {',
220
+ " [Console]::Error.WriteLine('tar: fauxnix: tar.exe not found (Windows 10+ ships bsdtar as tar.exe)')",
221
+ ' $script:fx_exit = 1',
222
+ '}',
223
+ ].join('\n');
224
+ };
225
+ /* ------------------------------------------------------------------ */
226
+ /* zip / unzip */
227
+ /* ------------------------------------------------------------------ */
228
+ const zip = (args) => {
229
+ const raw = args.map(wordToString);
230
+ let excludeNote = false;
231
+ const rest = [];
232
+ for (let i = 0; i < raw.length; i++) {
233
+ const t = raw[i];
234
+ if (t === '-x' || t === '--exclude') {
235
+ excludeNote = true;
236
+ if (i + 1 < args.length)
237
+ i++;
238
+ continue;
239
+ }
240
+ if (t.startsWith('--exclude=')) {
241
+ excludeNote = true;
242
+ continue;
243
+ }
244
+ if (t.startsWith('-') && t.length > 1 && !/^-\d/.test(t))
245
+ continue; // -r -q ... implicit
246
+ rest.push(args[i]);
247
+ }
248
+ const note = excludeNote
249
+ ? '[Console]::Error.WriteLine(' +
250
+ psStr('zip: fauxnix: -x/--exclude patterns are not supported, ignoring') +
251
+ ')\n'
252
+ : '';
253
+ if (rest.length < 2) {
254
+ return (note +
255
+ "[Console]::Error.WriteLine('zip error: Nothing to do! (fauxnix: usage: zip [-r] ARCHIVE FILES...)'); $script:fx_exit = 12");
256
+ }
257
+ const arc = operandExpr(rest[0]);
258
+ const inputs = rest.slice(1).map(operandExpr).join(', ');
259
+ return [
260
+ note + '$fx_arc = ' + arc,
261
+ '$fx_inputs = @(' + inputs + ')',
262
+ '$fx_valid = @()',
263
+ 'foreach ($fx_p in $fx_inputs) {',
264
+ ' if (Test-Path -LiteralPath $fx_p) { $fx_valid += $fx_p }',
265
+ " else { [Console]::Error.WriteLine('zip warning: name not matched: ' + $fx_p) }",
266
+ '}',
267
+ 'if ($fx_valid.Count -eq 0) {',
268
+ " [Console]::Error.WriteLine('zip error: Nothing to do!')",
269
+ ' $script:fx_exit = 12',
270
+ '} else {',
271
+ ' try {',
272
+ // Compress-Archive keeps the trailing path component as the archive root,
273
+ // always includes directory contents, and appends .zip when the extension
274
+ // is missing (GNU zip does not) — hence the rename dance.
275
+ " $fx_dst = $fx_arc",
276
+ ' $fx_rename = $false',
277
+ " if (-not $fx_arc.ToLower().EndsWith('.zip')) { $fx_dst = $fx_arc + '.zip'; $fx_rename = $true }",
278
+ ' Compress-Archive -Path $fx_valid -DestinationPath $fx_dst -Force -ErrorAction Stop',
279
+ // PS 5.1 Compress-Archive writes `\` entry separators; rewrite them to `/`
280
+ // so GNU unzip/bsdtar see proper directory entries.
281
+ ' Add-Type -AssemblyName System.IO.Compression.FileSystem',
282
+ ' $fx_z = [IO.Compression.ZipFile]::Open($fx_dst, \'Update\')',
283
+ ' try {',
284
+ ' foreach ($fx_e in @($fx_z.Entries)) {',
285
+ " if ($fx_e.FullName.Contains('\\')) {",
286
+ " $fx_ne = $fx_z.CreateEntry($fx_e.FullName.Replace('\\', '/'))",
287
+ ' $fx_os = $fx_ne.Open()',
288
+ ' $fx_is = $fx_e.Open()',
289
+ ' $fx_is.CopyTo($fx_os)',
290
+ ' $fx_is.Close()',
291
+ ' $fx_os.Close()',
292
+ ' $fx_e.Delete()',
293
+ ' }',
294
+ ' }',
295
+ ' } finally { $fx_z.Dispose() }',
296
+ ' if ($fx_rename) { Move-Item -LiteralPath $fx_dst -Destination $fx_arc -Force }',
297
+ " } catch { [Console]::Error.WriteLine('zip: fauxnix: ' + $_.Exception.Message); $script:fx_exit = 1 }",
298
+ '}',
299
+ ].join('\n');
300
+ };
301
+ const unzip = (args) => {
302
+ const raw = args.map(wordToString);
303
+ let list = false;
304
+ let over = false;
305
+ let dir = null;
306
+ const rest = [];
307
+ for (let i = 0; i < raw.length; i++) {
308
+ const t = raw[i];
309
+ if (t === '-d' && i + 1 < args.length) {
310
+ dir = args[i + 1];
311
+ i++;
312
+ continue;
313
+ }
314
+ if (t.startsWith('--directory=')) {
315
+ const dv = [{ kind: 'Text', text: t.slice('--directory='.length) }];
316
+ dir = dv;
317
+ continue;
318
+ }
319
+ if (t === '--directory' && i + 1 < args.length) {
320
+ dir = args[i + 1];
321
+ i++;
322
+ continue;
323
+ }
324
+ if (t.startsWith('-') && t.length > 1 && !/^-\d/.test(t)) {
325
+ if (t.includes('l'))
326
+ list = true;
327
+ if (t.includes('o'))
328
+ over = true;
329
+ continue;
330
+ }
331
+ rest.push(args[i]);
332
+ }
333
+ if (rest.length === 0) {
334
+ return "[Console]::Error.WriteLine('unzip: fauxnix: missing archive operand (usage: unzip ARCHIVE [-d DIR] [-o] [-l])'); $script:fx_exit = 1";
335
+ }
336
+ const arc = operandExpr(rest[0]);
337
+ const dirExpr = dir ? operandExpr(dir) : "'.'";
338
+ return [
339
+ 'Add-Type -AssemblyName System.IO.Compression.FileSystem',
340
+ '$fx_arc = ' + arc,
341
+ 'if (-not (Test-Path -LiteralPath $fx_arc -PathType Leaf)) {',
342
+ // GNU message shape: "unzip: cannot find x, x.zip or x.Z." (two spaces)
343
+ " [Console]::Error.WriteLine('unzip: cannot find ' + $fx_arc + ', ' + $fx_arc + '.zip or ' + $fx_arc + '.Z.')",
344
+ ' $script:fx_exit = 1',
345
+ '}',
346
+ 'else {',
347
+ list
348
+ ? [
349
+ ' $fx_z = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $fx_arc).ProviderPath)',
350
+ ' try {',
351
+ " 'Archive: ' + $fx_arc",
352
+ " ' Length Date Time Name'",
353
+ " '--------- ---------- ----- ----'",
354
+ ' $fx_tot = 0',
355
+ ' $fx_cnt = 0',
356
+ ' foreach ($fx_e in $fx_z.Entries) {',
357
+ " if (-not $fx_e.FullName.EndsWith('/')) { $fx_tot = $fx_tot + $fx_e.Length; $fx_cnt = $fx_cnt + 1 }",
358
+ " ('{0,9} {1} {2}' -f $fx_e.Length, $fx_e.LastWriteTime.ToString('MM-dd-yy HH:mm'), $fx_e.FullName.Replace('\\', '/'))",
359
+ ' }',
360
+ " '--------- ---------- ----- ----'",
361
+ " $fx_pl = 's'",
362
+ " if ($fx_cnt -eq 1) { $fx_pl = '' }",
363
+ " ((([string]$fx_tot).PadLeft(9)) + ' ' + $fx_cnt + ' file' + $fx_pl)",
364
+ ' } finally { $fx_z.Dispose() }',
365
+ ].join('\n')
366
+ : [
367
+ ' $fx_dir = ' + dirExpr,
368
+ ' try {',
369
+ ' Expand-Archive -LiteralPath $fx_arc -DestinationPath $fx_dir -Force:' + (over ? '$true' : '$false') + ' -ErrorAction Stop',
370
+ " } catch { [Console]::Error.WriteLine('unzip: fauxnix: ' + $_.Exception.Message + ' (use -o to overwrite)'); $script:fx_exit = 1 }",
371
+ ].join('\n'),
372
+ '}',
373
+ ].join('\n');
374
+ };
375
+ /* ------------------------------------------------------------------ */
376
+ /* exports */
377
+ /* ------------------------------------------------------------------ */
378
+ export const handlers = {
379
+ tar,
380
+ gzip,
381
+ gunzip,
382
+ zcat,
383
+ zip,
384
+ unzip,
385
+ };
@@ -0,0 +1,2 @@
1
+ import { Handler } from '../registry.js';
2
+ export declare const handlers: Record<string, Handler>;