@nimbus-sh/core 0.6.0 → 0.8.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 +47 -26
- package/dist/_shared/byte-stream.d.ts +5 -4
- package/dist/_shared/byte-stream.d.ts.map +1 -1
- package/dist/_shared/real-node-imports.d.ts +7 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -1
- package/dist/_shared/real-node-imports.js +8 -0
- package/dist/_shared/tarball.d.ts +24 -5
- package/dist/_shared/tarball.d.ts.map +1 -1
- package/dist/_shared/tarball.js +42 -5
- package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
- package/dist/_shared/vfs-write-ledger.js +22 -16
- package/dist/runtime/comment-strip.d.ts +32 -29
- package/dist/runtime/comment-strip.d.ts.map +1 -1
- package/dist/runtime/comment-strip.js +273 -51
- package/dist/runtime/cpython-runner.d.ts +1 -0
- package/dist/runtime/cpython-runner.d.ts.map +1 -1
- package/dist/runtime/cpython-runner.js +1 -1
- package/dist/runtime/esbuild-service.d.ts +38 -42
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +500 -388
- package/dist/runtime/javascript-ast.d.ts.map +1 -1
- package/dist/runtime/javascript-ast.js +45 -7
- package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-backend.js +18 -16
- package/dist/runtime/port-registry.d.ts +4 -2
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +60 -8
- package/dist/runtime/ruby-runner.d.ts +1 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -1
- package/dist/runtime/ruby-runner.js +1 -0
- package/dist/shell/stdin-adapter.d.ts +11 -0
- package/dist/shell/stdin-adapter.d.ts.map +1 -0
- package/dist/shell/stdin-adapter.js +59 -0
- package/dist/shell/unix-commands.d.ts +2 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +1078 -50
- package/dist/substrate/lifo/commands/io/dd.js +1 -1
- package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/net/curl.js +270 -54
- package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
- package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/npm.js +25 -43
- package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/head.js +2 -1
- package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/sed.js +499 -117
- package/dist/substrate/lifo/commands/types.d.ts +8 -4
- package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
- package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.js +122 -23
- package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
- package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/interpreter.js +79 -29
- package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
- package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/pipe.js +62 -18
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
- package/dist/vfs/sqlite-vfs.d.ts +19 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +106 -23
- package/dist/workspace/nimbus-workspace.d.ts +22 -1
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +19 -3
- package/dist/workspace/supervisor-op.d.ts +107 -0
- package/dist/workspace/supervisor-op.d.ts.map +1 -0
- package/dist/workspace/supervisor-op.js +224 -0
- package/package.json +2 -2
- package/src/_shared/byte-stream.ts +5 -4
- package/src/_shared/real-node-imports.ts +8 -0
- package/src/_shared/tarball.ts +60 -5
- package/src/_shared/vfs-write-ledger.ts +22 -16
- package/src/runtime/comment-strip.ts +218 -52
- package/src/runtime/cpython-runner.ts +2 -1
- package/src/runtime/esbuild-service.ts +572 -372
- package/src/runtime/javascript-ast.ts +40 -9
- package/src/runtime/opentui-wasm-backend.ts +19 -17
- package/src/runtime/port-registry.ts +66 -8
- package/src/runtime/ruby-runner.ts +2 -0
- package/src/shell/stdin-adapter.ts +58 -0
- package/src/shell/unix-commands.ts +1054 -50
- package/src/substrate/lifo/commands/io/dd.ts +2 -2
- package/src/substrate/lifo/commands/net/curl.ts +310 -58
- package/src/substrate/lifo/commands/system/npm.ts +55 -38
- package/src/substrate/lifo/commands/text/head.ts +2 -1
- package/src/substrate/lifo/commands/text/sed.ts +519 -109
- package/src/substrate/lifo/commands/types.ts +8 -5
- package/src/substrate/lifo/node-compat/zlib.ts +155 -26
- package/src/substrate/lifo/shell/interpreter.ts +74 -27
- package/src/substrate/lifo/shell/pipe.ts +65 -24
- package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
- package/src/vfs/sqlite-vfs.ts +104 -23
- package/src/workspace/nimbus-workspace.ts +30 -3
- package/src/workspace/supervisor-op.ts +309 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* uptime, tree, find, grep -r, head, tail, wc, diff, sort, uniq,
|
|
9
9
|
* sed (s///), awk (field extract), xargs, tee, chown, ln -s,
|
|
10
10
|
* du, man/help, basename, dirname, printf, true, false, seq, sleep,
|
|
11
|
-
* touch, stat, file, xxd, base64, sha256sum, id, hostname,
|
|
11
|
+
* touch, stat, file, xxd, od, hexdump, base64, sha256sum, id, hostname,
|
|
12
|
+
* realpath
|
|
12
13
|
*/
|
|
13
14
|
import { getSymlinkRegistry } from '../vfs/symlink-registry.js';
|
|
14
15
|
import { requireVfsCred } from '../runtime/os-contracts.js';
|
|
@@ -19,6 +20,7 @@ import { SinkWriter, streamRange } from '../_shared/byte-stream.js';
|
|
|
19
20
|
import { fileTypeChar, isCharacterDevice, } from '../substrate/lifo/kernel/vfs/index.js';
|
|
20
21
|
import { runSed } from '../substrate/lifo/commands/text/sed.js';
|
|
21
22
|
import { parseArgs } from '../substrate/lifo/utils/args.js';
|
|
23
|
+
import { encode } from '../substrate/lifo/utils/encoding.js';
|
|
22
24
|
import { findUnixGroupName, findUnixUserName, parseChownOwnership, } from './unix-accounts.js';
|
|
23
25
|
import { createSuCommand, createSudoCommand, createUmaskCommand } from './elevation-commands.js';
|
|
24
26
|
/**
|
|
@@ -212,6 +214,9 @@ const _CANONICAL_BIN_PATHS = {
|
|
|
212
214
|
bundler: '/usr/bin/bundler',
|
|
213
215
|
sh: '/usr/bin/sh',
|
|
214
216
|
bash: '/usr/bin/bash',
|
|
217
|
+
// Hex dumps — the real tools live under /usr/bin on Unix
|
|
218
|
+
od: '/usr/bin/od',
|
|
219
|
+
hexdump: '/usr/bin/hexdump',
|
|
215
220
|
// Framework CLIs
|
|
216
221
|
astro: '/usr/local/bin/astro',
|
|
217
222
|
nuxt: '/usr/local/bin/nuxt',
|
|
@@ -1649,10 +1654,21 @@ async function streamStdinBytes(ctx, writer, limit) {
|
|
|
1649
1654
|
let copied = 0;
|
|
1650
1655
|
while (copied < limit) {
|
|
1651
1656
|
const want = limit - copied;
|
|
1652
|
-
|
|
1657
|
+
let chunk;
|
|
1658
|
+
if (reader.readBytes) {
|
|
1659
|
+
chunk = await reader.readBytes(want);
|
|
1660
|
+
}
|
|
1661
|
+
else {
|
|
1662
|
+
// Text-only readers report EOF with null; encoding that null into an
|
|
1663
|
+
// empty chunk would spin the loop forever without advancing.
|
|
1664
|
+
const text = await reader.read();
|
|
1665
|
+
if (text === null)
|
|
1666
|
+
break;
|
|
1667
|
+
chunk = enc.encode(text);
|
|
1668
|
+
}
|
|
1653
1669
|
if (chunk === null)
|
|
1654
1670
|
break;
|
|
1655
|
-
const bytes =
|
|
1671
|
+
const bytes = chunk.subarray(0, want);
|
|
1656
1672
|
writer.write(bytes);
|
|
1657
1673
|
copied += bytes.length;
|
|
1658
1674
|
}
|
|
@@ -4458,56 +4474,1063 @@ function mkFile(vfs) {
|
|
|
4458
4474
|
return 0;
|
|
4459
4475
|
};
|
|
4460
4476
|
}
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4477
|
+
// ── Hex dumps: od, hexdump, xxd ─────────────────────────────────────────
|
|
4478
|
+
/**
|
|
4479
|
+
* Dump-tool byte counts: decimal, `0x` hex, leading-zero octal, and the
|
|
4480
|
+
* classic suffixes (`b` blocks of 512, K/KiB, KB, M, G — powers of 1024
|
|
4481
|
+
* except the round-decimal `KB`/`MB`/`GB` spellings). Null means the value
|
|
4482
|
+
* is not a count these tools accept.
|
|
4483
|
+
*/
|
|
4484
|
+
function parseDumpCount(value) {
|
|
4485
|
+
const match = /^(0[xX][0-9a-fA-F]+|0[0-7]*|[1-9][0-9]*)([bB]|[kKmMgGtT](?:i?[bB])?)?$/.exec(value);
|
|
4486
|
+
if (match === null)
|
|
4487
|
+
return null;
|
|
4488
|
+
const digits = match[1];
|
|
4489
|
+
const base = digits.startsWith('0x') || digits.startsWith('0X')
|
|
4490
|
+
? Number.parseInt(digits, 16)
|
|
4491
|
+
: /^0/.test(digits) ? Number.parseInt(digits, 8) : Number.parseInt(digits, 10);
|
|
4492
|
+
if (!Number.isSafeInteger(base) || base < 0)
|
|
4493
|
+
return null;
|
|
4494
|
+
if (match[2] === undefined || match[2] === '')
|
|
4495
|
+
return base;
|
|
4496
|
+
const scale = {
|
|
4497
|
+
b: 512, B: 512,
|
|
4498
|
+
k: 1024, K: 1024, KiB: 1024, kB: 1000, KB: 1000,
|
|
4499
|
+
m: 1024 ** 2, M: 1024 ** 2, MiB: 1024 ** 2, mB: 1000 ** 2, MB: 1000 ** 2,
|
|
4500
|
+
g: 1024 ** 3, G: 1024 ** 3, GiB: 1024 ** 3, gB: 1000 ** 3, GB: 1000 ** 3,
|
|
4501
|
+
t: 1024 ** 4, T: 1024 ** 4, TiB: 1024 ** 4, tB: 1000 ** 4, TB: 1000 ** 4,
|
|
4502
|
+
};
|
|
4503
|
+
const factor = scale[match[2]];
|
|
4504
|
+
if (factor === undefined)
|
|
4505
|
+
return null;
|
|
4506
|
+
const total = base * factor;
|
|
4507
|
+
return Number.isSafeInteger(total) ? total : null;
|
|
4508
|
+
}
|
|
4509
|
+
/** Little-endian word; a short final chunk reads its missing bytes as zero. */
|
|
4510
|
+
function leWord(chunk) {
|
|
4511
|
+
return (chunk[0] ?? 0) | ((chunk[1] ?? 0) << 8);
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4514
|
+
* Shared row suppression: a formatted row equal to the one before it prints
|
|
4515
|
+
* as `*`, and runs of repeats collapse into that single marker. GNU od and
|
|
4516
|
+
* util-linux hexdump suppress the address together with the row, so callers
|
|
4517
|
+
* classify the body alone and print the marker bare.
|
|
4518
|
+
*/
|
|
4519
|
+
class RowDedup {
|
|
4520
|
+
previous = null;
|
|
4521
|
+
starred = false;
|
|
4522
|
+
classify(row, verbose) {
|
|
4523
|
+
if (verbose || row !== this.previous) {
|
|
4524
|
+
this.previous = row;
|
|
4525
|
+
this.starred = false;
|
|
4526
|
+
return 'print';
|
|
4527
|
+
}
|
|
4528
|
+
if (this.starred)
|
|
4529
|
+
return 'skip';
|
|
4530
|
+
this.starred = true;
|
|
4531
|
+
return 'star';
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4535
|
+
* Row addresses follow the radix (`0000000` octal or decimal, `000000`
|
|
4536
|
+
* lowercase hex). The closing total-length line keeps uppercase hex digits —
|
|
4537
|
+
* `00001B`, not `00001b` — matching od on this host byte for byte.
|
|
4538
|
+
*/
|
|
4539
|
+
function odAddress(radix, offset, final) {
|
|
4540
|
+
if (radix === 'n')
|
|
4541
|
+
return '';
|
|
4542
|
+
if (radix === 'o')
|
|
4543
|
+
return offset.toString(8).padStart(7, '0');
|
|
4544
|
+
if (radix === 'd')
|
|
4545
|
+
return String(offset).padStart(7, '0');
|
|
4546
|
+
const digits = offset.toString(16);
|
|
4547
|
+
return (final ? digits.toUpperCase() : digits).padStart(6, '0');
|
|
4548
|
+
}
|
|
4549
|
+
/** `\0`-style escapes for `-tc`; other non-printables go out as `\NNN`. */
|
|
4550
|
+
const OD_CHAR_ESCAPES = {
|
|
4551
|
+
0: '\\0', 7: '\\a', 8: '\\b', 9: '\\t', 10: '\\n', 11: '\\v', 12: '\\f', 13: '\\r',
|
|
4552
|
+
};
|
|
4553
|
+
const OD_TYPES = {
|
|
4554
|
+
c: {
|
|
4555
|
+
width: 1,
|
|
4556
|
+
natural: 3,
|
|
4557
|
+
render: (c) => {
|
|
4558
|
+
const escaped = OD_CHAR_ESCAPES[c[0]];
|
|
4559
|
+
if (escaped !== undefined)
|
|
4560
|
+
return escaped.padStart(3);
|
|
4561
|
+
if (c[0] >= 32 && c[0] < 127)
|
|
4562
|
+
return String.fromCharCode(c[0]).padStart(3);
|
|
4563
|
+
return c[0].toString(8).padStart(3, '0');
|
|
4564
|
+
},
|
|
4565
|
+
},
|
|
4566
|
+
d1: { width: 1, natural: 4, render: (c) => String((c[0] << 24) >> 24).padStart(4) },
|
|
4567
|
+
d2: { width: 2, natural: 6, render: (c) => { const v = leWord(c); return String(v >= 0x8000 ? v - 0x10000 : v).padStart(6); } },
|
|
4568
|
+
u1: { width: 1, natural: 3, render: (c) => String(c[0]).padStart(3) },
|
|
4569
|
+
u2: { width: 2, natural: 5, render: (c) => String(leWord(c)).padStart(5) },
|
|
4570
|
+
o1: { width: 1, natural: 3, render: (c) => c[0].toString(8).padStart(3, '0') },
|
|
4571
|
+
o2: { width: 2, natural: 6, render: (c) => leWord(c).toString(8).padStart(6, '0') },
|
|
4572
|
+
x1: { width: 1, natural: 2, render: (c) => c[0].toString(16).padStart(2, '0') },
|
|
4573
|
+
x2: { width: 2, natural: 4, render: (c) => leWord(c).toString(16).padStart(4, '0') },
|
|
4574
|
+
};
|
|
4575
|
+
/**
|
|
4576
|
+
* Split a `-t` value into its concatenated specifications: a letter with an
|
|
4577
|
+
* optional size (`x1`, `d2`, `c`, …), so `-tx1c` yields x1 then c.
|
|
4578
|
+
*/
|
|
4579
|
+
function splitOdTypes(value) {
|
|
4580
|
+
const specs = [];
|
|
4581
|
+
const pattern = /([xoducXODUC])([01248]?)/g;
|
|
4582
|
+
let consumed = '';
|
|
4583
|
+
for (const match of value.matchAll(pattern)) {
|
|
4584
|
+
consumed += match[0];
|
|
4585
|
+
const letter = match[1].toLowerCase();
|
|
4586
|
+
const size = match[2] === '' ? '' : match[2];
|
|
4587
|
+
if (letter === 'a') {
|
|
4588
|
+
specs.push('c');
|
|
4589
|
+
continue;
|
|
4467
4590
|
}
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4591
|
+
specs.push(`${letter}${size}`);
|
|
4592
|
+
}
|
|
4593
|
+
return consumed === value ? specs : [value];
|
|
4594
|
+
}
|
|
4595
|
+
/** `-A[o|d|x|n]`, cumulative `-t<spec>`, `-N<count>`, `-v`. */
|
|
4596
|
+
function parseOdArgs(args) {
|
|
4597
|
+
let radix = 'o';
|
|
4598
|
+
const types = [];
|
|
4599
|
+
let limit;
|
|
4600
|
+
let verbose = false;
|
|
4601
|
+
const files = [];
|
|
4602
|
+
for (let i = 0; i < args.length; i++) {
|
|
4603
|
+
const arg = args[i];
|
|
4604
|
+
if (arg === '--') {
|
|
4605
|
+
files.push(...args.slice(i + 1));
|
|
4606
|
+
break;
|
|
4607
|
+
}
|
|
4608
|
+
if (!arg.startsWith('-') || arg === '-') {
|
|
4609
|
+
files.push(arg);
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
const flag = arg[1];
|
|
4613
|
+
let value = arg.slice(2);
|
|
4614
|
+
if (flag === 'v' && value === '') {
|
|
4615
|
+
verbose = true;
|
|
4616
|
+
continue;
|
|
4617
|
+
}
|
|
4618
|
+
if (flag !== 'A' && flag !== 't' && flag !== 'N') {
|
|
4619
|
+
return { error: `od: invalid option -- '${flag}'` };
|
|
4620
|
+
}
|
|
4621
|
+
if (value === '') {
|
|
4622
|
+
value = args[i + 1];
|
|
4623
|
+
if (value !== undefined)
|
|
4624
|
+
i++;
|
|
4625
|
+
}
|
|
4626
|
+
if (value === undefined || value === '') {
|
|
4627
|
+
return { error: `od: option requires an argument -- '${flag}'` };
|
|
4628
|
+
}
|
|
4629
|
+
if (flag === 'A') {
|
|
4630
|
+
// Exactly one radix letter per option (`-Aod` is not two glued flags);
|
|
4631
|
+
// repeated valid options are last-wins, like GNU od.
|
|
4632
|
+
if (value.length !== 1 || !'odxn'.includes(value)) {
|
|
4633
|
+
return { error: `od: Radix must be one of [o, d, x, n], got: ${value}` };
|
|
4476
4634
|
}
|
|
4477
|
-
|
|
4635
|
+
radix = value;
|
|
4478
4636
|
}
|
|
4479
|
-
|
|
4480
|
-
|
|
4637
|
+
else if (flag === 't') {
|
|
4638
|
+
// One -t may carry concatenated specifications (`-tx1c`); repeated
|
|
4639
|
+
// -t flags accumulate too. Both render in request order.
|
|
4640
|
+
for (const spec of splitOdTypes(value)) {
|
|
4641
|
+
if (!(spec in OD_TYPES))
|
|
4642
|
+
return { error: `od: unsupported type specification '${value}'` };
|
|
4643
|
+
types.push(spec);
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
else {
|
|
4647
|
+
const parsed = parseDumpCount(value);
|
|
4648
|
+
if (parsed === null || parsed < 0) {
|
|
4649
|
+
return { error: `od: invalid number of bytes '${value}'` };
|
|
4650
|
+
}
|
|
4651
|
+
limit = parsed;
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
if (types.length === 0)
|
|
4655
|
+
types.push('o2');
|
|
4656
|
+
return { radix, types, limit, verbose, files };
|
|
4657
|
+
}
|
|
4658
|
+
/**
|
|
4659
|
+
* Sequential bytes for a dump tool: each operand in order, stdin once,
|
|
4660
|
+
* bounded range reads for files and bounded pulls for pipes. Only one
|
|
4661
|
+
* window of bytes is held at a time, so unbounded tools stream forever
|
|
4662
|
+
* instead of growing silently, and limits stop collection early.
|
|
4663
|
+
*/
|
|
4664
|
+
class DumpByteSource {
|
|
4665
|
+
ctx;
|
|
4666
|
+
label;
|
|
4667
|
+
operands;
|
|
4668
|
+
operandIndex = 0;
|
|
4669
|
+
remaining;
|
|
4670
|
+
currentPath = '';
|
|
4671
|
+
cursor = 0;
|
|
4672
|
+
haveOpen = false;
|
|
4673
|
+
stdinMode = false;
|
|
4674
|
+
stdinUsed = false;
|
|
4675
|
+
probedFirst = false;
|
|
4676
|
+
// Drained-string stdin is encoded once; pulls slice the encoded bytes so
|
|
4677
|
+
// multibyte input never advances past bytes it did not return.
|
|
4678
|
+
stdinBytes = null;
|
|
4679
|
+
stdinCursor = 0;
|
|
4680
|
+
stdinOverflow = [];
|
|
4681
|
+
failures = 0;
|
|
4682
|
+
opened = 0;
|
|
4683
|
+
total = 0;
|
|
4684
|
+
constructor(ctx, label, files, limit) {
|
|
4685
|
+
this.ctx = ctx;
|
|
4686
|
+
this.label = label;
|
|
4687
|
+
this.operands = files.length > 0 ? files : [undefined];
|
|
4688
|
+
this.remaining = limit ?? Number.POSITIVE_INFINITY;
|
|
4689
|
+
}
|
|
4690
|
+
get failed() {
|
|
4691
|
+
return this.failures > 0;
|
|
4692
|
+
}
|
|
4693
|
+
/** Every named operand failed to open — distinct from successful empty. */
|
|
4694
|
+
get failedAll() {
|
|
4695
|
+
return this.operands.length > 0 && this.opened === 0 && this.failures > 0;
|
|
4696
|
+
}
|
|
4697
|
+
async openNextOperand() {
|
|
4698
|
+
while (this.operandIndex < this.operands.length) {
|
|
4699
|
+
const file = this.operands[this.operandIndex++];
|
|
4700
|
+
try {
|
|
4701
|
+
if (file === undefined || file === '-') {
|
|
4702
|
+
if (this.stdinUsed)
|
|
4703
|
+
continue; // second '-' reads stdin already at EOF
|
|
4704
|
+
this.stdinMode = true;
|
|
4705
|
+
this.opened++; // an empty stdin still counts as successfully opened
|
|
4706
|
+
return true;
|
|
4707
|
+
}
|
|
4708
|
+
// Probe the file now so per-operand errors surface exactly once.
|
|
4709
|
+
// Named operands read through ctx.vfs — the mount-aware seam the
|
|
4710
|
+
// host handed the command — so /dev and other mounts resolve while
|
|
4711
|
+
// an embedder's credentialed view keeps its authorization.
|
|
4712
|
+
this.currentPath = absolutePath(this.ctx.cwd, file);
|
|
4713
|
+
this.ctx.vfs.readRange(this.currentPath, 0, 1);
|
|
4714
|
+
this.cursor = 0;
|
|
4715
|
+
this.haveOpen = true;
|
|
4716
|
+
this.opened++;
|
|
4717
|
+
return true;
|
|
4718
|
+
}
|
|
4719
|
+
catch (error) {
|
|
4720
|
+
this.ctx.stderr.write(`${this.label}: ${file}: ${fsErrorMessage(error)}\n`);
|
|
4721
|
+
this.failures++;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
return false;
|
|
4725
|
+
}
|
|
4726
|
+
closeCurrent() {
|
|
4727
|
+
this.haveOpen = false;
|
|
4728
|
+
this.stdinMode = false;
|
|
4729
|
+
this.currentPath = '';
|
|
4730
|
+
this.cursor = 0;
|
|
4731
|
+
}
|
|
4732
|
+
/** One bounded pull; drained strings are encoded once and sliced by byte. */
|
|
4733
|
+
async stdinPull(max) {
|
|
4734
|
+
if (typeof this.ctx.stdin === 'string') {
|
|
4735
|
+
if (this.stdinBytes === null)
|
|
4736
|
+
this.stdinBytes = enc.encode(this.ctx.stdin);
|
|
4737
|
+
if (this.stdinCursor >= this.stdinBytes.length)
|
|
4738
|
+
return null;
|
|
4739
|
+
const end = Math.min(this.stdinCursor + max, this.stdinBytes.length);
|
|
4740
|
+
const chunk = this.stdinBytes.subarray(this.stdinCursor, end);
|
|
4741
|
+
this.stdinCursor = end;
|
|
4742
|
+
return chunk;
|
|
4743
|
+
}
|
|
4744
|
+
const reader = this.ctx.stdin;
|
|
4745
|
+
if (typeof reader?.read !== 'function')
|
|
4746
|
+
return null;
|
|
4747
|
+
// read/readAll-only embedders lose nothing: overflow bytes from a bounded
|
|
4748
|
+
// pull wait in stdinOverflow until the next one. A live stream hands back
|
|
4749
|
+
// its first available chunk — waiting to fill `max` would stall a sparse
|
|
4750
|
+
// producer that has written one byte and not the rest.
|
|
4751
|
+
let chunk;
|
|
4752
|
+
if (this.stdinOverflow.length > 0) {
|
|
4753
|
+
chunk = this.stdinOverflow.shift() ?? null;
|
|
4754
|
+
}
|
|
4755
|
+
else if (reader.readBytes) {
|
|
4756
|
+
chunk = await reader.readBytes(Math.min(65536, max));
|
|
4757
|
+
}
|
|
4758
|
+
else {
|
|
4759
|
+
const text = await reader.read();
|
|
4760
|
+
chunk = text === null ? null : enc.encode(text);
|
|
4761
|
+
}
|
|
4762
|
+
if (chunk === null || chunk.length === 0)
|
|
4763
|
+
return null;
|
|
4764
|
+
if (chunk.length <= max)
|
|
4765
|
+
return chunk;
|
|
4766
|
+
this.stdinOverflow.unshift(chunk.subarray(max));
|
|
4767
|
+
return chunk.subarray(0, max);
|
|
4768
|
+
}
|
|
4769
|
+
/**
|
|
4770
|
+
* Up to `max` bytes, filling across chunks and operands. A row-oriented
|
|
4771
|
+
* caller wants the whole row before it formats anything, exactly as GNU od
|
|
4772
|
+
* fills its 16-byte buffer, so this waits for the count it asked for.
|
|
4773
|
+
*/
|
|
4774
|
+
async take(max) {
|
|
4775
|
+
return this.collect(max, false);
|
|
4776
|
+
}
|
|
4777
|
+
/**
|
|
4778
|
+
* Up to `max` bytes, waiting only for the first ones to arrive. A caller
|
|
4779
|
+
* that formats whatever has landed uses this: file operands still answer
|
|
4780
|
+
* in bulk, while a live stream is never waited on for bytes a sparse
|
|
4781
|
+
* producer has not written yet.
|
|
4782
|
+
*/
|
|
4783
|
+
async takeReady(max) {
|
|
4784
|
+
return this.collect(max, true);
|
|
4785
|
+
}
|
|
4786
|
+
async collect(max, ready) {
|
|
4787
|
+
if (!this.probedFirst) {
|
|
4788
|
+
// The first named operand must be attempted even under a zero limit,
|
|
4789
|
+
// so `-l0 /missing` reports the open error instead of succeeding.
|
|
4790
|
+
this.probedFirst = true;
|
|
4791
|
+
if (this.operands.length > 0 && !(await this.openNextOperand())) {
|
|
4792
|
+
return null;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
if (max <= 0)
|
|
4796
|
+
return new Uint8Array(0);
|
|
4797
|
+
const parts = [];
|
|
4798
|
+
let got = 0;
|
|
4799
|
+
while (got < max) {
|
|
4800
|
+
if (this.remaining <= 0)
|
|
4801
|
+
break;
|
|
4802
|
+
if (!this.haveOpen && !this.stdinMode) {
|
|
4803
|
+
if (!(await this.openNextOperand()))
|
|
4804
|
+
break;
|
|
4805
|
+
}
|
|
4806
|
+
const want = Math.min(max - got, this.remaining, 65536);
|
|
4807
|
+
let chunk;
|
|
4808
|
+
if (this.stdinMode) {
|
|
4809
|
+
chunk = await this.stdinPull(want);
|
|
4810
|
+
if (chunk === null || chunk.length === 0) {
|
|
4811
|
+
this.stdinUsed = true;
|
|
4812
|
+
this.closeCurrent();
|
|
4813
|
+
continue;
|
|
4814
|
+
}
|
|
4815
|
+
// A live stream hands back what it has: `takeReady` stops here so a
|
|
4816
|
+
// sparse producer keeps rendering, while `take` loops for the rest of
|
|
4817
|
+
// the row it was asked for.
|
|
4818
|
+
parts.push(chunk);
|
|
4819
|
+
got += chunk.length;
|
|
4820
|
+
this.total += chunk.length;
|
|
4821
|
+
this.remaining -= chunk.length;
|
|
4822
|
+
if (ready)
|
|
4823
|
+
break;
|
|
4824
|
+
continue;
|
|
4825
|
+
}
|
|
4826
|
+
// File operands keep filling: range reads are bulk and cost nothing
|
|
4827
|
+
// extra, and a block may span consecutive operands.
|
|
4828
|
+
chunk = this.ctx.vfs.readRange(this.currentPath, this.cursor, want);
|
|
4829
|
+
if (chunk.length === 0) {
|
|
4830
|
+
this.closeCurrent();
|
|
4831
|
+
continue;
|
|
4832
|
+
}
|
|
4833
|
+
this.cursor += chunk.length;
|
|
4834
|
+
const take = chunk.length <= want ? chunk : chunk.subarray(0, want);
|
|
4835
|
+
parts.push(take);
|
|
4836
|
+
got += take.length;
|
|
4837
|
+
this.total += take.length;
|
|
4838
|
+
this.remaining -= take.length;
|
|
4839
|
+
}
|
|
4840
|
+
if (parts.length === 0)
|
|
4841
|
+
return null;
|
|
4842
|
+
if (parts.length === 1)
|
|
4843
|
+
return parts[0];
|
|
4844
|
+
const out = new Uint8Array(got);
|
|
4845
|
+
let at = 0;
|
|
4846
|
+
for (const part of parts) {
|
|
4847
|
+
out.set(part, at);
|
|
4848
|
+
at += part.length;
|
|
4849
|
+
}
|
|
4850
|
+
return out;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
function mkOd() {
|
|
4854
|
+
return async (ctx) => {
|
|
4855
|
+
const parsed = parseOdArgs(ctx.args);
|
|
4856
|
+
if ('error' in parsed) {
|
|
4857
|
+
ctx.stderr.write(`${parsed.error}\n`);
|
|
4481
4858
|
return 1;
|
|
4482
4859
|
}
|
|
4860
|
+
const src = new DumpByteSource(ctx, 'od', parsed.files, parsed.limit);
|
|
4861
|
+
const specs = parsed.types.map((name) => OD_TYPES[name]);
|
|
4862
|
+
// With several -t types every item shares one column width (the widest
|
|
4863
|
+
// natural width plus one); each type prints on its own continuation
|
|
4864
|
+
// line indented under the address — uutils od's grid on this host.
|
|
4865
|
+
const columnWidth = Math.max(...specs.map((spec) => spec.natural)) + 1;
|
|
4866
|
+
const renderRow = (row) => specs.map((spec) => {
|
|
4867
|
+
const items = [];
|
|
4868
|
+
for (let i = 0; i < row.length; i += spec.width) {
|
|
4869
|
+
items.push(spec.render(row.subarray(i, Math.min(i + spec.width, row.length))));
|
|
4870
|
+
}
|
|
4871
|
+
if (specs.length === 1)
|
|
4872
|
+
return items.join(' ');
|
|
4873
|
+
return items.map((item) => item.padStart(columnWidth)).join('');
|
|
4874
|
+
});
|
|
4875
|
+
const dedup = new RowDedup();
|
|
4876
|
+
while (true) {
|
|
4877
|
+
const row = await src.take(16);
|
|
4878
|
+
if (row === null || row.length === 0)
|
|
4879
|
+
break;
|
|
4880
|
+
const address = odAddress(parsed.radix, src.total - row.length, false);
|
|
4881
|
+
const lines = renderRow(row);
|
|
4882
|
+
const key = lines.join('\n');
|
|
4883
|
+
const indent = address === '' ? '' : ' '.repeat(7);
|
|
4884
|
+
const rendered = specs.length === 1
|
|
4885
|
+
? `${address} ${lines[0]}`
|
|
4886
|
+
: [`${address}${lines[0]}`, ...lines.slice(1).map((line) => `${indent}${line}`)].join('\n');
|
|
4887
|
+
switch (dedup.classify(key, parsed.verbose)) {
|
|
4888
|
+
case 'print':
|
|
4889
|
+
ctx.stdout.write(`${rendered}\n`);
|
|
4890
|
+
break;
|
|
4891
|
+
case 'star':
|
|
4892
|
+
ctx.stdout.write('*\n');
|
|
4893
|
+
break;
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
if (parsed.radix !== 'n') {
|
|
4897
|
+
if (src.failedAll)
|
|
4898
|
+
return 1;
|
|
4899
|
+
ctx.stdout.write(`${odAddress(parsed.radix, src.total, true)}\n`);
|
|
4900
|
+
}
|
|
4901
|
+
return src.failed ? 1 : 0;
|
|
4483
4902
|
};
|
|
4484
4903
|
}
|
|
4485
|
-
|
|
4904
|
+
const HEXDUMP_WORD_SIZES = { '1': 1, '2': 2, '4': 4, C: 1 };
|
|
4905
|
+
/** Field widths an empty iteration pads to, mirroring util-linux. */
|
|
4906
|
+
const HEXDUMP_DIGIT_WIDTHS = {
|
|
4907
|
+
'1': { x: 2, o: 3, d: 3 },
|
|
4908
|
+
'2': { x: 4, o: 6, d: 5 },
|
|
4909
|
+
'4': { x: 8, o: 11, d: 10 },
|
|
4910
|
+
};
|
|
4911
|
+
/** Shared empty unit: a conversion past end-of-input sees no bytes at all. */
|
|
4912
|
+
const HEXDUMP_EMPTY_UNIT = new Uint8Array(0);
|
|
4913
|
+
/** Source bytes fetched per pull while streaming `-e` blocks. */
|
|
4914
|
+
const HEXDUMP_PULL_BYTES = 4096;
|
|
4915
|
+
/** Digits an address directive can render: `Number.MAX_SAFE_INTEGER` in octal. */
|
|
4916
|
+
const HEXDUMP_MAX_ADDRESS_DIGITS = 20;
|
|
4486
4917
|
/**
|
|
4487
|
-
*
|
|
4488
|
-
*
|
|
4489
|
-
*
|
|
4490
|
-
*
|
|
4491
|
-
* Also resolves ctx.stdin from a stream object to a string.
|
|
4492
|
-
* The shell passes stdin as an object with .readAll() when piping,
|
|
4493
|
-
* but our commands expect a plain string.
|
|
4918
|
+
* Characters one `-e` block may render. A block is held twice while it is
|
|
4919
|
+
* classified — the line and its dedup key — so this ceiling bounds two
|
|
4920
|
+
* strings of ~2 MiB UTF-16 each, small beside a Durable Object's memory and
|
|
4921
|
+
* far above any format that dumps real data.
|
|
4494
4922
|
*/
|
|
4923
|
+
const HEXDUMP_MAX_BLOCK_CHARS = 1 << 20;
|
|
4924
|
+
// util-linux rejects escaped delimiters inside -e units rather than
|
|
4925
|
+
// decoding them, so `\"` is deliberately absent here.
|
|
4926
|
+
const HEXDUMP_ESCAPES = {
|
|
4927
|
+
n: '\n', t: '\t', r: '\r', '\\': '\\', '0': '\0',
|
|
4928
|
+
};
|
|
4929
|
+
function parseHexdumpDirectives(fmt) {
|
|
4930
|
+
const segments = [];
|
|
4931
|
+
let text = '';
|
|
4932
|
+
const flush = () => { if (text !== '') {
|
|
4933
|
+
segments.push(text);
|
|
4934
|
+
text = '';
|
|
4935
|
+
} };
|
|
4936
|
+
const bad = (what) => ({ error: `hexdump: bad format {${what}}` });
|
|
4937
|
+
for (let i = 0; i < fmt.length; i++) {
|
|
4938
|
+
const ch = fmt[i];
|
|
4939
|
+
if (ch === '\\') {
|
|
4940
|
+
const esc = fmt[++i];
|
|
4941
|
+
if (esc === undefined || !(esc in HEXDUMP_ESCAPES))
|
|
4942
|
+
return bad(`\\${esc ?? ''}`);
|
|
4943
|
+
text += HEXDUMP_ESCAPES[esc];
|
|
4944
|
+
continue;
|
|
4945
|
+
}
|
|
4946
|
+
if (ch !== '%') {
|
|
4947
|
+
text += ch;
|
|
4948
|
+
continue;
|
|
4949
|
+
}
|
|
4950
|
+
flush();
|
|
4951
|
+
const directive = { kind: 'byte', leftAlign: false, zeroPad: false, width: undefined, precision: undefined };
|
|
4952
|
+
let j = i + 1;
|
|
4953
|
+
while (fmt[j] === '-' || fmt[j] === '0') {
|
|
4954
|
+
if (fmt[j] === '-')
|
|
4955
|
+
directive.leftAlign = true;
|
|
4956
|
+
else
|
|
4957
|
+
directive.zeroPad = true;
|
|
4958
|
+
j++;
|
|
4959
|
+
}
|
|
4960
|
+
let digits = '';
|
|
4961
|
+
while (fmt[j] >= '0' && fmt[j] <= '9')
|
|
4962
|
+
digits += fmt[j++];
|
|
4963
|
+
if (digits !== '') {
|
|
4964
|
+
directive.width = Number(digits);
|
|
4965
|
+
// A width outside the safe-integer range can never render; reject the
|
|
4966
|
+
// format here rather than attempting an unbounded allocation later.
|
|
4967
|
+
if (!Number.isSafeInteger(directive.width))
|
|
4968
|
+
return bad(`%${fmt.slice(i + 1, j + 1)}`);
|
|
4969
|
+
}
|
|
4970
|
+
if (fmt[j] === '.') {
|
|
4971
|
+
let prec = '';
|
|
4972
|
+
j++;
|
|
4973
|
+
while (fmt[j] >= '0' && fmt[j] <= '9')
|
|
4974
|
+
prec += fmt[j++];
|
|
4975
|
+
directive.precision = prec === '' ? 0 : Number(prec);
|
|
4976
|
+
if (!Number.isSafeInteger(directive.precision))
|
|
4977
|
+
return bad(`%${fmt.slice(i + 1, j + 1)}`);
|
|
4978
|
+
}
|
|
4979
|
+
if (fmt[j] === '_' && fmt[j + 1] === 'a' && 'dxo'.includes(fmt[j + 2])) {
|
|
4980
|
+
directive.kind = 'addr';
|
|
4981
|
+
directive.radix = fmt[j + 2];
|
|
4982
|
+
i = j + 2;
|
|
4983
|
+
}
|
|
4984
|
+
else if (j < fmt.length && 'xXduoc'.includes(fmt[j])) {
|
|
4985
|
+
directive.conv = fmt[j];
|
|
4986
|
+
i = j;
|
|
4987
|
+
}
|
|
4988
|
+
else {
|
|
4989
|
+
return bad(`%${fmt.slice(i + 1, j + 1)}`);
|
|
4990
|
+
}
|
|
4991
|
+
segments.push(directive);
|
|
4992
|
+
}
|
|
4993
|
+
flush();
|
|
4994
|
+
return { segments };
|
|
4995
|
+
}
|
|
4996
|
+
function parseHexdumpPieces(value) {
|
|
4997
|
+
const pieces = [];
|
|
4998
|
+
let pending = null;
|
|
4999
|
+
let i = 0;
|
|
5000
|
+
while (i < value.length) {
|
|
5001
|
+
const ch = value[i];
|
|
5002
|
+
if (ch === ' ' || ch === '\t') {
|
|
5003
|
+
i++;
|
|
5004
|
+
continue;
|
|
5005
|
+
}
|
|
5006
|
+
if (ch >= '0' && ch <= '9') {
|
|
5007
|
+
let digits = '';
|
|
5008
|
+
while (i < value.length && value[i] >= '0' && value[i] <= '9')
|
|
5009
|
+
digits += value[i++];
|
|
5010
|
+
if (value[i] !== '/')
|
|
5011
|
+
return { error: `hexdump: bad format {${value}}` };
|
|
5012
|
+
const sizeChar = value[i + 1] ?? '';
|
|
5013
|
+
const size = HEXDUMP_WORD_SIZES[sizeChar];
|
|
5014
|
+
if (size === undefined)
|
|
5015
|
+
return { error: `hexdump: bad format {${digits}/${sizeChar}}` };
|
|
5016
|
+
const count = Number(digits);
|
|
5017
|
+
if (!Number.isSafeInteger(count) || count <= 0) {
|
|
5018
|
+
return { error: `hexdump: bad format {${digits}/${sizeChar}}` };
|
|
5019
|
+
}
|
|
5020
|
+
pending = { count, size };
|
|
5021
|
+
i += 2;
|
|
5022
|
+
continue;
|
|
5023
|
+
}
|
|
5024
|
+
if (ch !== '"' && ch !== "'")
|
|
5025
|
+
return { error: `hexdump: bad format {${value.slice(i)}}` };
|
|
5026
|
+
// util-linux refuses an escaped quote inside a unit instead of decoding
|
|
5027
|
+
// it, and names the whole specification when it does.
|
|
5028
|
+
let close = -1;
|
|
5029
|
+
for (let k = i + 1; k < value.length; k++) {
|
|
5030
|
+
const c = value[k];
|
|
5031
|
+
if (c === '\\') {
|
|
5032
|
+
if (value[k + 1] === ch) {
|
|
5033
|
+
close = -2;
|
|
5034
|
+
break;
|
|
5035
|
+
}
|
|
5036
|
+
k++;
|
|
5037
|
+
continue;
|
|
5038
|
+
}
|
|
5039
|
+
if (c === ch) {
|
|
5040
|
+
close = k;
|
|
5041
|
+
break;
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
if (close < 0)
|
|
5045
|
+
return { error: `hexdump: bad format {${value}}` };
|
|
5046
|
+
const parsed = parseHexdumpDirectives(value.slice(i + 1, close));
|
|
5047
|
+
if ('error' in parsed)
|
|
5048
|
+
return parsed;
|
|
5049
|
+
const conversions = parsed.segments.filter((segment) => typeof segment !== 'string' && segment.kind === 'byte').length;
|
|
5050
|
+
// util-linux refuses a byte count feeding more than one conversion.
|
|
5051
|
+
if (conversions > 1) {
|
|
5052
|
+
return { error: 'hexdump: byte count with multiple conversion characters' };
|
|
5053
|
+
}
|
|
5054
|
+
// %c defaults to one byte and only accepts one; other conversions
|
|
5055
|
+
// default to four.
|
|
5056
|
+
const soleConv = conversions === 1
|
|
5057
|
+
? parsed.segments.find((segment) => typeof segment !== 'string' && segment.kind === 'byte').conv
|
|
5058
|
+
: undefined;
|
|
5059
|
+
if (soleConv === 'c' && pending !== null && pending.size !== 1) {
|
|
5060
|
+
return { error: 'hexdump: bad byte count for conversion character c' };
|
|
5061
|
+
}
|
|
5062
|
+
pieces.push({
|
|
5063
|
+
segments: parsed.segments,
|
|
5064
|
+
count: pending?.count ?? 1,
|
|
5065
|
+
size: pending?.size ?? (soleConv === 'c' ? 1 : 4),
|
|
5066
|
+
consumes: conversions > 0,
|
|
5067
|
+
});
|
|
5068
|
+
pending = null;
|
|
5069
|
+
i = close + 1;
|
|
5070
|
+
}
|
|
5071
|
+
if (pending !== null)
|
|
5072
|
+
return { error: `hexdump: bad format {${value}}` };
|
|
5073
|
+
return { pieces };
|
|
5074
|
+
}
|
|
5075
|
+
function hexdumpFormatNumber(directive, digits) {
|
|
5076
|
+
// fprintf rules: precision pads the magnitude with zeros, a sign always
|
|
5077
|
+
// sits in front of that padding, '-' alignment overrides '0', and an
|
|
5078
|
+
// explicit precision disables '0' field padding entirely.
|
|
5079
|
+
let sign = '';
|
|
5080
|
+
let magnitude = digits;
|
|
5081
|
+
if (magnitude.startsWith('-')) {
|
|
5082
|
+
sign = '-';
|
|
5083
|
+
magnitude = magnitude.slice(1);
|
|
5084
|
+
}
|
|
5085
|
+
if (directive.precision !== undefined)
|
|
5086
|
+
magnitude = magnitude.padStart(directive.precision, '0');
|
|
5087
|
+
const width = directive.width ?? 0;
|
|
5088
|
+
const value = sign + magnitude;
|
|
5089
|
+
if (directive.leftAlign)
|
|
5090
|
+
return value.padEnd(width);
|
|
5091
|
+
if (directive.zeroPad && directive.precision === undefined && width > 0) {
|
|
5092
|
+
return sign + magnitude.padStart(width - sign.length, '0');
|
|
5093
|
+
}
|
|
5094
|
+
return value.padStart(width);
|
|
5095
|
+
}
|
|
5096
|
+
/** Field width an iteration reserves, so missing ones pad like util-linux. */
|
|
5097
|
+
function hexdumpFieldWidth(directive, size) {
|
|
5098
|
+
if (directive.kind === 'addr')
|
|
5099
|
+
return directive.width ?? 0;
|
|
5100
|
+
if (directive.conv === 'c')
|
|
5101
|
+
return directive.width ?? 1;
|
|
5102
|
+
const family = directive.conv === 'x' || directive.conv === 'X'
|
|
5103
|
+
? 'x'
|
|
5104
|
+
: directive.conv === 'o' ? 'o' : 'd';
|
|
5105
|
+
return directive.width ?? HEXDUMP_DIGIT_WIDTHS[String(size)][family];
|
|
5106
|
+
}
|
|
4495
5107
|
/**
|
|
4496
|
-
*
|
|
4497
|
-
*
|
|
4498
|
-
*
|
|
5108
|
+
* Render one `-e` block straight off the source, one unit at a time. A
|
|
5109
|
+
* directive past the end of input renders as field-width spaces rather than
|
|
5110
|
+
* dropping its slot, and address directives report the offset of the next
|
|
5111
|
+
* byte to display. The dedup key repeats all of it except addresses, so
|
|
5112
|
+
* repeat suppression ignores where each block sits.
|
|
5113
|
+
*
|
|
5114
|
+
* util-linux's nospace rule: a unit repeated more than once drops the single
|
|
5115
|
+
* trailing whitespace character of its own format text on its LAST
|
|
5116
|
+
* repetition, EOF padding included. Exactly that one character goes, which
|
|
5117
|
+
* is why `3/1 "%02x "` keeps one of its two spaces, `2/1 "%02x\t"` keeps
|
|
5118
|
+
* the padding that follows the dropped tab, and a unit repeated once keeps
|
|
5119
|
+
* its spacing verbatim.
|
|
5120
|
+
*
|
|
5121
|
+
* Input arrives unit-sized: nothing collects count*size bytes however large
|
|
5122
|
+
* the repetition count is, and parseHexdumpArgs has already refused any
|
|
5123
|
+
* format whose block could outgrow {@link HEXDUMP_MAX_BLOCK_CHARS}.
|
|
4499
5124
|
*
|
|
4500
|
-
*
|
|
4501
|
-
*
|
|
4502
|
-
* bytes to a string (same as wrap — terminal stdin's
|
|
4503
|
-
* buffer-then-close pattern doesn't match streaming).
|
|
4504
|
-
* - If ctx.stdin is a pipe reader (has .read), PASS IT THROUGH
|
|
4505
|
-
* unchanged so the command can read line-by-line. The command
|
|
4506
|
-
* is responsible for terminating itself (e.g. head -n N stops
|
|
4507
|
-
* after N lines, triggering the pipeline-abort cascade from
|
|
4508
|
-
* SHELL-R6-2).
|
|
4509
|
-
* - String stdin / other shapes: same as wrap.
|
|
5125
|
+
* Returns the rendered line, its address-free key, and the source bytes the
|
|
5126
|
+
* block consumed; null once a block consumes nothing, i.e. true end.
|
|
4510
5127
|
*/
|
|
5128
|
+
async function hexdumpRenderBlock(pieces, pullUnit, blockStart) {
|
|
5129
|
+
let line = '';
|
|
5130
|
+
let key = '';
|
|
5131
|
+
let pos = 0;
|
|
5132
|
+
let consumed = 0;
|
|
5133
|
+
let ended = false;
|
|
5134
|
+
for (const piece of pieces) {
|
|
5135
|
+
const iterations = piece.consumes ? piece.count : 1;
|
|
5136
|
+
const tail = piece.segments[piece.segments.length - 1];
|
|
5137
|
+
const dropsTailSpace = iterations > 1
|
|
5138
|
+
&& typeof tail === 'string'
|
|
5139
|
+
&& /[ \t\n\r\v\f]$/.test(tail);
|
|
5140
|
+
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
5141
|
+
const lastIteration = iteration === iterations - 1;
|
|
5142
|
+
let unit = HEXDUMP_EMPTY_UNIT;
|
|
5143
|
+
if (piece.consumes && !ended) {
|
|
5144
|
+
unit = await pullUnit(piece.size);
|
|
5145
|
+
if (unit.length < piece.size)
|
|
5146
|
+
ended = true;
|
|
5147
|
+
}
|
|
5148
|
+
const missing = piece.consumes && unit.length === 0;
|
|
5149
|
+
// Literals always render; only byte conversions pad when input ran
|
|
5150
|
+
// out, so a trailing literal still reaches the line on short blocks.
|
|
5151
|
+
for (let index = 0; index < piece.segments.length; index++) {
|
|
5152
|
+
const segment = piece.segments[index];
|
|
5153
|
+
if (typeof segment === 'string') {
|
|
5154
|
+
const text = dropsTailSpace && lastIteration && index === piece.segments.length - 1
|
|
5155
|
+
? segment.slice(0, -1)
|
|
5156
|
+
: segment;
|
|
5157
|
+
line += text;
|
|
5158
|
+
key += text;
|
|
5159
|
+
continue;
|
|
5160
|
+
}
|
|
5161
|
+
if (segment.kind === 'addr') {
|
|
5162
|
+
const shown = blockStart + pos;
|
|
5163
|
+
const digits = segment.radix === 'd'
|
|
5164
|
+
? String(shown)
|
|
5165
|
+
: shown.toString(segment.radix === 'o' ? 8 : 16);
|
|
5166
|
+
line += hexdumpFormatNumber(segment, digits);
|
|
5167
|
+
continue;
|
|
5168
|
+
}
|
|
5169
|
+
if (missing) {
|
|
5170
|
+
const pad = ' '.repeat(hexdumpFieldWidth(segment, piece.size));
|
|
5171
|
+
line += pad;
|
|
5172
|
+
key += pad;
|
|
5173
|
+
continue;
|
|
5174
|
+
}
|
|
5175
|
+
let value = 0;
|
|
5176
|
+
for (let b = piece.size - 1; b >= 0; b--)
|
|
5177
|
+
value = value * 256 + (unit[b] ?? 0);
|
|
5178
|
+
pos += piece.size;
|
|
5179
|
+
if (segment.conv === 'c') {
|
|
5180
|
+
const text = hexdumpFormatNumber(segment, String.fromCharCode(value & 0xff));
|
|
5181
|
+
line += text;
|
|
5182
|
+
key += text;
|
|
5183
|
+
continue;
|
|
5184
|
+
}
|
|
5185
|
+
let digits;
|
|
5186
|
+
if (segment.conv === 'd') {
|
|
5187
|
+
const signBit = 256 ** piece.size / 2;
|
|
5188
|
+
digits = String(value >= signBit ? value - signBit * 2 : value);
|
|
5189
|
+
}
|
|
5190
|
+
else if (segment.conv === 'u') {
|
|
5191
|
+
digits = String(value);
|
|
5192
|
+
}
|
|
5193
|
+
else if (segment.conv === 'o') {
|
|
5194
|
+
digits = value.toString(8);
|
|
5195
|
+
}
|
|
5196
|
+
else {
|
|
5197
|
+
digits = value.toString(16);
|
|
5198
|
+
if (segment.conv === 'X')
|
|
5199
|
+
digits = digits.toUpperCase();
|
|
5200
|
+
}
|
|
5201
|
+
const text = hexdumpFormatNumber(segment, digits);
|
|
5202
|
+
line += text;
|
|
5203
|
+
key += text;
|
|
5204
|
+
}
|
|
5205
|
+
consumed += unit.length;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
if (consumed === 0)
|
|
5209
|
+
return null;
|
|
5210
|
+
return { line, key, consumed };
|
|
5211
|
+
}
|
|
5212
|
+
/** Body columns for the fixed modes; the caller prefixes the address. */
|
|
5213
|
+
function hexdumpFixedBody(mode, row) {
|
|
5214
|
+
if (mode === 'C') {
|
|
5215
|
+
const group1 = Array.from(row.subarray(0, 8), (b) => b.toString(16).padStart(2, '0'));
|
|
5216
|
+
const group2 = Array.from(row.subarray(8), (b) => b.toString(16).padStart(2, '0'));
|
|
5217
|
+
const columns = group1.join(' ') + (group2.length > 0 ? ' ' + group2.join(' ') : '');
|
|
5218
|
+
const bar = Array.from(row, (b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : '.')).join('');
|
|
5219
|
+
return { body: columns.padEnd(50), bar };
|
|
5220
|
+
}
|
|
5221
|
+
if (mode === 'default') {
|
|
5222
|
+
const words = [];
|
|
5223
|
+
for (let i = 0; i < row.length; i += 2) {
|
|
5224
|
+
words.push(leWord(row.subarray(i)).toString(16).padStart(4, '0'));
|
|
5225
|
+
}
|
|
5226
|
+
return { body: words.join(' ').padEnd(39), bar: null };
|
|
5227
|
+
}
|
|
5228
|
+
const slots = [];
|
|
5229
|
+
for (let i = 0; i < 16; i += 2) {
|
|
5230
|
+
const chunk = row.subarray(i);
|
|
5231
|
+
if (chunk.length === 0) {
|
|
5232
|
+
slots.push(' '.repeat(8));
|
|
5233
|
+
continue;
|
|
5234
|
+
}
|
|
5235
|
+
const rendered = mode === 'x'
|
|
5236
|
+
? leWord(chunk).toString(16).padStart(4, '0')
|
|
5237
|
+
: mode === 'd'
|
|
5238
|
+
? String(leWord(chunk)).padStart(5, '0')
|
|
5239
|
+
: leWord(chunk).toString(8).padStart(6, '0');
|
|
5240
|
+
slots.push(rendered.padStart(i === 0 ? 7 : 8));
|
|
5241
|
+
}
|
|
5242
|
+
return { body: slots.join(''), bar: null };
|
|
5243
|
+
}
|
|
5244
|
+
/** Format flags override each other (last wins); `-e` replaces them all. */
|
|
5245
|
+
function parseHexdumpArgs(args) {
|
|
5246
|
+
let mode = 'default';
|
|
5247
|
+
let pieces = null;
|
|
5248
|
+
let length;
|
|
5249
|
+
let verbose = false;
|
|
5250
|
+
const files = [];
|
|
5251
|
+
for (let i = 0; i < args.length; i++) {
|
|
5252
|
+
const arg = args[i];
|
|
5253
|
+
if (arg === '--') {
|
|
5254
|
+
files.push(...args.slice(i + 1));
|
|
5255
|
+
break;
|
|
5256
|
+
}
|
|
5257
|
+
if (!arg.startsWith('-') || arg === '-') {
|
|
5258
|
+
files.push(arg);
|
|
5259
|
+
continue;
|
|
5260
|
+
}
|
|
5261
|
+
const flag = arg[1];
|
|
5262
|
+
const inlineValue = arg.slice(2);
|
|
5263
|
+
if (flag === 'C' || flag === 'x' || flag === 'd' || flag === 'o') {
|
|
5264
|
+
if (inlineValue !== '')
|
|
5265
|
+
return { error: `hexdump: invalid option -- '${flag}'` };
|
|
5266
|
+
mode = flag;
|
|
5267
|
+
continue;
|
|
5268
|
+
}
|
|
5269
|
+
if (flag === 'v' && inlineValue === '') {
|
|
5270
|
+
verbose = true;
|
|
5271
|
+
continue;
|
|
5272
|
+
}
|
|
5273
|
+
if (flag !== 'e' && flag !== 'n') {
|
|
5274
|
+
return { error: `hexdump: invalid option -- '${flag}'` };
|
|
5275
|
+
}
|
|
5276
|
+
const value = inlineValue || args[++i];
|
|
5277
|
+
if (value === undefined || value === '') {
|
|
5278
|
+
return { error: `hexdump: option requires an argument -- '${flag}'` };
|
|
5279
|
+
}
|
|
5280
|
+
if (flag === 'n') {
|
|
5281
|
+
const parsed = parseDumpCount(value);
|
|
5282
|
+
if (parsed === null || parsed < 0) {
|
|
5283
|
+
return { error: `hexdump: invalid length '${value}'` };
|
|
5284
|
+
}
|
|
5285
|
+
length = parsed;
|
|
5286
|
+
continue;
|
|
5287
|
+
}
|
|
5288
|
+
if (pieces !== null)
|
|
5289
|
+
return { error: 'hexdump: only one -e format is supported' };
|
|
5290
|
+
const parsed = parseHexdumpPieces(value);
|
|
5291
|
+
if ('error' in parsed)
|
|
5292
|
+
return parsed;
|
|
5293
|
+
if (!parsed.pieces.some((piece) => piece.consumes)) {
|
|
5294
|
+
return { error: `hexdump: bad format {${value}}` };
|
|
5295
|
+
}
|
|
5296
|
+
// The block is held twice while it is classified, so a format whose
|
|
5297
|
+
// rendering cannot fit is refused here — before a byte is ever read.
|
|
5298
|
+
let blockChars = 0;
|
|
5299
|
+
for (const piece of parsed.pieces) {
|
|
5300
|
+
let perIteration = 0;
|
|
5301
|
+
for (const segment of piece.segments) {
|
|
5302
|
+
if (typeof segment === 'string') {
|
|
5303
|
+
perIteration += segment.length;
|
|
5304
|
+
continue;
|
|
5305
|
+
}
|
|
5306
|
+
if (segment.kind === 'addr') {
|
|
5307
|
+
perIteration += Math.max(segment.width ?? 0, HEXDUMP_MAX_ADDRESS_DIGITS);
|
|
5308
|
+
continue;
|
|
5309
|
+
}
|
|
5310
|
+
// A narrow field cannot shrink a value: `%1u` over four bytes still
|
|
5311
|
+
// renders ten characters. Size each conversion by the widest of its
|
|
5312
|
+
// field width, its precision plus a sign, and the natural maximum
|
|
5313
|
+
// for its byte size.
|
|
5314
|
+
const digits = HEXDUMP_DIGIT_WIDTHS[String(piece.size)];
|
|
5315
|
+
const natural = segment.conv === 'c'
|
|
5316
|
+
? 1
|
|
5317
|
+
: segment.conv === 'd'
|
|
5318
|
+
? digits.d + 1
|
|
5319
|
+
: segment.conv === 'u'
|
|
5320
|
+
? digits.d
|
|
5321
|
+
: segment.conv === 'o'
|
|
5322
|
+
? digits.o
|
|
5323
|
+
: digits.x;
|
|
5324
|
+
perIteration += Math.max(segment.width ?? 0, natural, (segment.precision ?? 0) + 1);
|
|
5325
|
+
}
|
|
5326
|
+
blockChars += (piece.consumes ? piece.count : 1) * perIteration;
|
|
5327
|
+
}
|
|
5328
|
+
if (blockChars > HEXDUMP_MAX_BLOCK_CHARS) {
|
|
5329
|
+
return {
|
|
5330
|
+
error: `hexdump: format renders ${blockChars} characters per block, over the ${HEXDUMP_MAX_BLOCK_CHARS} limit`,
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
5333
|
+
pieces = parsed.pieces;
|
|
5334
|
+
}
|
|
5335
|
+
return { mode, pieces, length, verbose, files };
|
|
5336
|
+
}
|
|
5337
|
+
function mkHexdump() {
|
|
5338
|
+
return async (ctx) => {
|
|
5339
|
+
const parsed = parseHexdumpArgs(ctx.args);
|
|
5340
|
+
if ('error' in parsed) {
|
|
5341
|
+
ctx.stderr.write(`${parsed.error}\n`);
|
|
5342
|
+
return 1;
|
|
5343
|
+
}
|
|
5344
|
+
const src = new DumpByteSource(ctx, 'hexdump', parsed.files, parsed.length);
|
|
5345
|
+
const dedup = new RowDedup();
|
|
5346
|
+
if (parsed.pieces !== null) {
|
|
5347
|
+
const lastPiece = parsed.pieces[parsed.pieces.length - 1];
|
|
5348
|
+
const lastSegment = lastPiece.segments[lastPiece.segments.length - 1];
|
|
5349
|
+
// Repeat suppression needs line boundaries; free-form formats emit
|
|
5350
|
+
// the whole stream, which is what -v spells on util-linux.
|
|
5351
|
+
const lineStructured = typeof lastSegment === 'string' && lastSegment.endsWith('\n');
|
|
5352
|
+
// Units arrive from a small stash fed well ahead of demand, so
|
|
5353
|
+
// per-unit pulls amortize into large source reads without ever
|
|
5354
|
+
// holding count*size bytes for a block.
|
|
5355
|
+
let pending = HEXDUMP_EMPTY_UNIT;
|
|
5356
|
+
const pullUnit = async (size) => {
|
|
5357
|
+
while (pending.length < size) {
|
|
5358
|
+
// Ready reads: a file answers the whole block in one range read,
|
|
5359
|
+
// and a live producer answers with whatever it has already written.
|
|
5360
|
+
const chunk = await src.takeReady(Math.max(size - pending.length, HEXDUMP_PULL_BYTES));
|
|
5361
|
+
if (chunk === null || chunk.length === 0)
|
|
5362
|
+
break;
|
|
5363
|
+
const merged = new Uint8Array(pending.length + chunk.length);
|
|
5364
|
+
merged.set(pending);
|
|
5365
|
+
merged.set(chunk, pending.length);
|
|
5366
|
+
pending = merged;
|
|
5367
|
+
}
|
|
5368
|
+
const unit = pending.subarray(0, size);
|
|
5369
|
+
pending = pending.subarray(unit.length);
|
|
5370
|
+
return unit;
|
|
5371
|
+
};
|
|
5372
|
+
let offset = 0;
|
|
5373
|
+
while (true) {
|
|
5374
|
+
const block = await hexdumpRenderBlock(parsed.pieces, pullUnit, offset);
|
|
5375
|
+
if (block === null)
|
|
5376
|
+
break;
|
|
5377
|
+
offset += block.consumed;
|
|
5378
|
+
if (!lineStructured) {
|
|
5379
|
+
ctx.stdout.write(block.line);
|
|
5380
|
+
continue;
|
|
5381
|
+
}
|
|
5382
|
+
switch (dedup.classify(block.key, parsed.verbose)) {
|
|
5383
|
+
case 'print':
|
|
5384
|
+
ctx.stdout.write(block.line);
|
|
5385
|
+
break;
|
|
5386
|
+
case 'star':
|
|
5387
|
+
ctx.stdout.write('*\n');
|
|
5388
|
+
break;
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
if (src.failedAll)
|
|
5392
|
+
ctx.stderr.write('hexdump: all input file arguments failed\n');
|
|
5393
|
+
return src.failed ? 1 : 0;
|
|
5394
|
+
}
|
|
5395
|
+
const wide = parsed.mode === 'C';
|
|
5396
|
+
while (true) {
|
|
5397
|
+
const row = await src.take(16);
|
|
5398
|
+
if (row === null || row.length === 0)
|
|
5399
|
+
break;
|
|
5400
|
+
const { body, bar } = hexdumpFixedBody(parsed.mode, row);
|
|
5401
|
+
const address = (src.total - row.length).toString(16).padStart(wide ? 8 : 7, '0');
|
|
5402
|
+
switch (dedup.classify(body, parsed.verbose)) {
|
|
5403
|
+
case 'print':
|
|
5404
|
+
ctx.stdout.write(wide ? `${address} ${body}|${bar}|\n` : `${address} ${body}\n`);
|
|
5405
|
+
break;
|
|
5406
|
+
case 'star':
|
|
5407
|
+
ctx.stdout.write('*\n');
|
|
5408
|
+
break;
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
if (src.failedAll)
|
|
5412
|
+
ctx.stderr.write('hexdump: all input file arguments failed\n');
|
|
5413
|
+
if (src.total > 0) {
|
|
5414
|
+
ctx.stdout.write(`${src.total.toString(16).padStart(wide ? 8 : 7, '0')}\n`);
|
|
5415
|
+
}
|
|
5416
|
+
return src.failed ? 1 : 0;
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
// ── xxd ─────────────────────────────────────────────────────────────────
|
|
5420
|
+
/**
|
|
5421
|
+
* `xxd [FILE [-] [OUTFILE]]` — pipelines are xxd's primary use, so stdin is
|
|
5422
|
+
* read when no input operand is given or `-` names it. A second positional
|
|
5423
|
+
* operand receives the dump as a file, like real xxd. `-l N` limits the dump
|
|
5424
|
+
* (decimal, 0x hex, leading-zero octal, with count suffixes); `-p` emits
|
|
5425
|
+
* continuous hex in bounded 30-byte rows. The default row layout predates
|
|
5426
|
+
* this fix and is preserved verbatim.
|
|
5427
|
+
*/
|
|
5428
|
+
function mkXxd() {
|
|
5429
|
+
return async (ctx) => {
|
|
5430
|
+
let plain = false;
|
|
5431
|
+
let limit;
|
|
5432
|
+
const operands = [];
|
|
5433
|
+
for (let i = 0; i < ctx.args.length; i++) {
|
|
5434
|
+
const arg = ctx.args[i];
|
|
5435
|
+
if (arg === '-p') {
|
|
5436
|
+
plain = true;
|
|
5437
|
+
continue;
|
|
5438
|
+
}
|
|
5439
|
+
if (arg === '-' || !arg.startsWith('-')) {
|
|
5440
|
+
if (operands.length >= 2) {
|
|
5441
|
+
ctx.stderr.write(`xxd: extra operand '${arg}'\n`);
|
|
5442
|
+
return 1;
|
|
5443
|
+
}
|
|
5444
|
+
operands.push(arg);
|
|
5445
|
+
continue;
|
|
5446
|
+
}
|
|
5447
|
+
if (arg === '-l' || arg.startsWith('-l')) {
|
|
5448
|
+
const value = arg === '-l' ? ctx.args[++i] : arg.slice(2);
|
|
5449
|
+
const parsed = value === undefined ? null : parseDumpCount(value);
|
|
5450
|
+
if (parsed === null || parsed < 0) {
|
|
5451
|
+
ctx.stderr.write(`xxd: invalid length value '${value ?? ''}'\n`);
|
|
5452
|
+
return 1;
|
|
5453
|
+
}
|
|
5454
|
+
limit = parsed;
|
|
5455
|
+
continue;
|
|
5456
|
+
}
|
|
5457
|
+
ctx.stderr.write(`xxd: invalid option -- '${arg.replace(/^-+/, '')}'\n`);
|
|
5458
|
+
return 1;
|
|
5459
|
+
}
|
|
5460
|
+
// Prime the source FIRST: pull the initial window (surfacing any open
|
|
5461
|
+
// error) before the output file exists to truncate.
|
|
5462
|
+
const rowSize = plain ? 30 : 16;
|
|
5463
|
+
const src = new DumpByteSource(ctx, 'xxd', operands.slice(0, 1), limit);
|
|
5464
|
+
const firstWindow = await src.take(rowSize);
|
|
5465
|
+
if (src.failedAll || (src.failed && src.opened === 0))
|
|
5466
|
+
return 1;
|
|
5467
|
+
// A second operand names the output file; `-` there means stdout. It
|
|
5468
|
+
// routes through ctx.vfs like every other named path, so dumps may land
|
|
5469
|
+
// on devices and mounts as they do on Unix.
|
|
5470
|
+
const output = operands[1];
|
|
5471
|
+
const outAbs = output !== undefined && output !== '-' ? absolutePath(ctx.cwd, output) : null;
|
|
5472
|
+
let offset = 0;
|
|
5473
|
+
let fileOffset = 0;
|
|
5474
|
+
let pending = [];
|
|
5475
|
+
let pendingBytes = 0;
|
|
5476
|
+
let writeFailed = false;
|
|
5477
|
+
const flush = () => {
|
|
5478
|
+
if (pending.length === 0 || writeFailed || outAbs === null)
|
|
5479
|
+
return;
|
|
5480
|
+
try {
|
|
5481
|
+
ctx.vfs.writeRange(outAbs, fileOffset, encode(pending.join('')));
|
|
5482
|
+
}
|
|
5483
|
+
catch (error) {
|
|
5484
|
+
ctx.stderr.write(`xxd: ${output}: ${fsErrorMessage(error)}\n`);
|
|
5485
|
+
writeFailed = true;
|
|
5486
|
+
return;
|
|
5487
|
+
}
|
|
5488
|
+
fileOffset += pendingBytes;
|
|
5489
|
+
pending = [];
|
|
5490
|
+
pendingBytes = 0;
|
|
5491
|
+
};
|
|
5492
|
+
const renderWindow = (rowOffset, window) => {
|
|
5493
|
+
if (plain) {
|
|
5494
|
+
let line = '';
|
|
5495
|
+
for (const byte of window)
|
|
5496
|
+
line += byte.toString(16).padStart(2, '0');
|
|
5497
|
+
return `${line}\n`;
|
|
5498
|
+
}
|
|
5499
|
+
const pairs = Array.from(window, (b) => b.toString(16).padStart(2, '0')).join(' ');
|
|
5500
|
+
const ascii = Array.from(window, (b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : '.')).join('');
|
|
5501
|
+
return `${rowOffset.toString(16).padStart(8, '0')}: ${pairs.padEnd(48)} ${ascii}\n`;
|
|
5502
|
+
};
|
|
5503
|
+
if (outAbs !== null) {
|
|
5504
|
+
// Input proved readable above, so truncating here cannot destroy data
|
|
5505
|
+
// on a failed dump.
|
|
5506
|
+
try {
|
|
5507
|
+
ctx.vfs.writeFile(outAbs, '');
|
|
5508
|
+
}
|
|
5509
|
+
catch (error) {
|
|
5510
|
+
ctx.stderr.write(`xxd: ${output}: ${fsErrorMessage(error)}\n`);
|
|
5511
|
+
return 1;
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
let window = firstWindow;
|
|
5515
|
+
while (window !== null && window.length > 0 && !writeFailed) {
|
|
5516
|
+
const text = renderWindow(offset, window);
|
|
5517
|
+
offset += window.length;
|
|
5518
|
+
if (outAbs !== null) {
|
|
5519
|
+
pending.push(text);
|
|
5520
|
+
pendingBytes += text.length;
|
|
5521
|
+
if (pendingBytes >= 65536)
|
|
5522
|
+
flush();
|
|
5523
|
+
}
|
|
5524
|
+
else {
|
|
5525
|
+
ctx.stdout.write(text);
|
|
5526
|
+
}
|
|
5527
|
+
window = await src.take(rowSize);
|
|
5528
|
+
}
|
|
5529
|
+
if (outAbs !== null)
|
|
5530
|
+
flush();
|
|
5531
|
+
return src.failed || writeFailed ? 1 : 0;
|
|
5532
|
+
};
|
|
5533
|
+
}
|
|
4511
5534
|
function wrapStreaming(fn) {
|
|
4512
5535
|
return async (ctx) => {
|
|
4513
5536
|
try {
|
|
@@ -4515,10 +5538,10 @@ function wrapStreaming(fn) {
|
|
|
4515
5538
|
const stdinObj = ctx.stdin;
|
|
4516
5539
|
const isTerminalStdin = typeof stdinObj.feed === 'function';
|
|
4517
5540
|
if (isTerminalStdin) {
|
|
4518
|
-
const
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
5541
|
+
const drainable = stdinObj;
|
|
5542
|
+
ctx.stdin = typeof drainable.drainBuffered === 'function'
|
|
5543
|
+
? drainable.drainBuffered()
|
|
5544
|
+
: '';
|
|
4522
5545
|
}
|
|
4523
5546
|
// else: leave as pipe reader for the command to handle.
|
|
4524
5547
|
}
|
|
@@ -4561,10 +5584,10 @@ function wrap(fn) {
|
|
|
4561
5584
|
// first command on a line; non-empty if the user typed
|
|
4562
5585
|
// text + Enter before the command was dispatched). DO NOT
|
|
4563
5586
|
// await — that would wait for the user's next Ctrl-D.
|
|
4564
|
-
const
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
5587
|
+
const drainable = stdinObj;
|
|
5588
|
+
ctx.stdin = typeof drainable.drainBuffered === 'function'
|
|
5589
|
+
? drainable.drainBuffered()
|
|
5590
|
+
: '';
|
|
4568
5591
|
}
|
|
4569
5592
|
else if (typeof stdinObj.readAll === 'function') {
|
|
4570
5593
|
// Pipe reader — upstream will close() after writing, so
|
|
@@ -4635,7 +5658,12 @@ export function registerUnixCommands(registry, sqliteVfs) {
|
|
|
4635
5658
|
registry.register('readlink', wrap(withInvocationVfs(sqliteVfs, mkReadlink)));
|
|
4636
5659
|
registry.register('sha256sum', wrap(withInvocationVfs(sqliteVfs, mkSha256sum)));
|
|
4637
5660
|
registry.register('file', wrap(withInvocationVfs(sqliteVfs, mkFile)));
|
|
4638
|
-
|
|
5661
|
+
// od/hexdump/xxd read operands and sinks through ctx.vfs — the
|
|
5662
|
+
// mount-aware seam the host hands every command — so they need no
|
|
5663
|
+
// invocation-scoped raw view of their own.
|
|
5664
|
+
registry.register('xxd', wrapStreaming(mkXxd()));
|
|
5665
|
+
registry.register('od', wrapStreaming(mkOd()));
|
|
5666
|
+
registry.register('hexdump', wrapStreaming(mkHexdump()));
|
|
4639
5667
|
registry.register('chown', wrap(mkChown(sqliteVfs)));
|
|
4640
5668
|
// ln — symlink stub (no-ops on VFS but doesn't error)
|
|
4641
5669
|
/**
|