@nimbus-sh/core 0.5.0 → 0.7.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/cache-stats.d.ts +23 -1
- package/dist/_shared/cache-stats.d.ts.map +1 -1
- package/dist/_shared/cache-stats.js +13 -1
- package/dist/_shared/exports-resolver.d.ts +1 -1
- package/dist/_shared/exports-resolver.js +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/retry.js +1 -1
- package/dist/_shared/tarball.d.ts +23 -5
- package/dist/_shared/tarball.d.ts.map +1 -1
- package/dist/_shared/tarball.js +38 -5
- package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
- package/dist/_shared/vfs-write-ledger.js +22 -16
- package/dist/constants.d.ts +5 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -56
- package/dist/runtime/assets-loader.js +1 -1
- package/dist/runtime/comment-strip.d.ts +0 -35
- package/dist/runtime/comment-strip.d.ts.map +1 -1
- package/dist/runtime/comment-strip.js +48 -20
- 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 +494 -197
- 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/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +1 -1
- 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 +26 -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/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +1 -1
- package/dist/vfs/sqlite-vfs.d.ts +21 -30
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +113 -29
- package/dist/workspace/nimbus-workspace.d.ts +15 -0
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +20 -3
- package/dist/workspace/supervisor-op.d.ts +23 -0
- package/dist/workspace/supervisor-op.d.ts.map +1 -0
- package/dist/workspace/supervisor-op.js +92 -0
- package/package.json +3 -2
- package/src/_shared/byte-stream.ts +5 -4
- package/src/_shared/cache-stats.ts +27 -1
- package/src/_shared/exports-resolver.ts +1 -1
- package/src/_shared/real-node-imports.ts +8 -0
- package/src/_shared/retry.ts +1 -1
- package/src/_shared/tarball.ts +56 -5
- package/src/_shared/vfs-write-ledger.ts +22 -16
- package/src/constants.ts +30 -60
- package/src/runtime/assets-loader.ts +1 -1
- package/src/runtime/comment-strip.ts +42 -23
- package/src/runtime/esbuild-service.ts +566 -217
- 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/virtual-socket-kernel.generated.ts +1 -1
- 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 +58 -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/seed-project.ts +3 -3
- package/src/vfs/sqlite-vfs.ts +128 -64
- package/src/workspace/nimbus-workspace.ts +26 -2
- package/src/workspace/supervisor-op.ts +117 -0
- package/dist/_shared/install-phase.d.ts +0 -47
- package/dist/_shared/install-phase.d.ts.map +0 -1
- package/dist/_shared/install-phase.js +0 -1
- package/dist/_shared/rpc-dispose.d.ts +0 -4
- package/dist/_shared/rpc-dispose.d.ts.map +0 -1
- package/dist/_shared/rpc-dispose.js +0 -30
- package/dist/_shared/w7-frame.d.ts +0 -79
- package/dist/_shared/w7-frame.d.ts.map +0 -1
- package/dist/_shared/w7-frame.js +0 -840
- package/dist/_shared/weighted-credit-pool.d.ts +0 -61
- package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
- package/dist/_shared/weighted-credit-pool.js +0 -196
- package/dist/observability/diag-counters.d.ts +0 -236
- package/dist/observability/diag-counters.d.ts.map +0 -1
- package/dist/observability/diag-counters.js +0 -195
- package/dist/observability/heap-estimate.d.ts +0 -225
- package/dist/observability/heap-estimate.d.ts.map +0 -1
- package/dist/observability/heap-estimate.js +0 -185
- package/dist/observability/heavy-alloc-coord.d.ts +0 -93
- package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
- package/dist/observability/heavy-alloc-coord.js +0 -181
- package/dist/observability/oom-classify.d.ts +0 -138
- package/dist/observability/oom-classify.d.ts.map +0 -1
- package/dist/observability/oom-classify.js +0 -268
- package/dist/observability/oom-discriminator.d.ts +0 -176
- package/dist/observability/oom-discriminator.d.ts.map +0 -1
- package/dist/observability/oom-discriminator.js +0 -272
- package/src/_shared/install-phase.ts +0 -56
- package/src/_shared/rpc-dispose.ts +0 -31
- package/src/_shared/w7-frame.ts +0 -1013
- package/src/_shared/weighted-credit-pool.ts +0 -244
- package/src/observability/diag-counters.ts +0 -343
- package/src/observability/heap-estimate.ts +0 -331
- package/src/observability/heavy-alloc-coord.ts +0 -241
- package/src/observability/oom-classify.ts +0 -260
- package/src/observability/oom-discriminator.ts +0 -405
|
@@ -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
|
|
|
14
15
|
import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
import type { Command, CommandInputStream } from '../substrate/lifo/commands/types.js';
|
|
28
29
|
import { runSed } from '../substrate/lifo/commands/text/sed.js';
|
|
29
30
|
import { parseArgs } from '../substrate/lifo/utils/args.js';
|
|
31
|
+
import { encode } from '../substrate/lifo/utils/encoding.js';
|
|
30
32
|
import {
|
|
31
33
|
findUnixGroupName,
|
|
32
34
|
findUnixUserName,
|
|
@@ -37,11 +39,10 @@ import { createSuCommand, createSudoCommand, createUmaskCommand } from './elevat
|
|
|
37
39
|
/**
|
|
38
40
|
* stdin as the shell hands it over: a pipe reader, whose `readAll` resolves
|
|
39
41
|
* once upstream closes, or the terminal's own stream, which stays open past
|
|
40
|
-
* the command and
|
|
42
|
+
* the command and is drained in place via `drainBuffered`.
|
|
41
43
|
*/
|
|
42
44
|
type ShellStdin = CommandInputStream & {
|
|
43
45
|
feed?(text: string): void;
|
|
44
|
-
buffer?: string[];
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -322,6 +323,9 @@ const _CANONICAL_BIN_PATHS: Record<string, string> = {
|
|
|
322
323
|
bundler: '/usr/bin/bundler',
|
|
323
324
|
sh: '/usr/bin/sh',
|
|
324
325
|
bash: '/usr/bin/bash',
|
|
326
|
+
// Hex dumps — the real tools live under /usr/bin on Unix
|
|
327
|
+
od: '/usr/bin/od',
|
|
328
|
+
hexdump: '/usr/bin/hexdump',
|
|
325
329
|
// Framework CLIs
|
|
326
330
|
astro: '/usr/local/bin/astro',
|
|
327
331
|
nuxt: '/usr/local/bin/nuxt',
|
|
@@ -1595,15 +1599,27 @@ async function streamStdinBytes(ctx: Ctx, writer: SinkWriter, limit: number): Pr
|
|
|
1595
1599
|
writer.write(enc.encode(stdin).subarray(0, limit));
|
|
1596
1600
|
return;
|
|
1597
1601
|
}
|
|
1598
|
-
const reader = stdin as {
|
|
1602
|
+
const reader = stdin as {
|
|
1603
|
+
read?: () => Promise<string | null>;
|
|
1604
|
+
readBytes?: (n: number) => Promise<Uint8Array | null>;
|
|
1605
|
+
};
|
|
1599
1606
|
if (typeof reader?.read !== 'function') return;
|
|
1600
1607
|
|
|
1601
1608
|
let copied = 0;
|
|
1602
1609
|
while (copied < limit) {
|
|
1603
1610
|
const want = limit - copied;
|
|
1604
|
-
|
|
1611
|
+
let chunk: Uint8Array | null;
|
|
1612
|
+
if (reader.readBytes) {
|
|
1613
|
+
chunk = await reader.readBytes(want);
|
|
1614
|
+
} else {
|
|
1615
|
+
// Text-only readers report EOF with null; encoding that null into an
|
|
1616
|
+
// empty chunk would spin the loop forever without advancing.
|
|
1617
|
+
const text = await reader.read();
|
|
1618
|
+
if (text === null) break;
|
|
1619
|
+
chunk = enc.encode(text);
|
|
1620
|
+
}
|
|
1605
1621
|
if (chunk === null) break;
|
|
1606
|
-
const bytes =
|
|
1622
|
+
const bytes = chunk.subarray(0, want);
|
|
1607
1623
|
writer.write(bytes);
|
|
1608
1624
|
copied += bytes.length;
|
|
1609
1625
|
}
|
|
@@ -4191,51 +4207,1034 @@ function mkFile(vfs: UnixVfs): CmdFn {
|
|
|
4191
4207
|
};
|
|
4192
4208
|
}
|
|
4193
4209
|
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4210
|
+
// ── Hex dumps: od, hexdump, xxd ─────────────────────────────────────────
|
|
4211
|
+
|
|
4212
|
+
/**
|
|
4213
|
+
* Dump-tool byte counts: decimal, `0x` hex, leading-zero octal, and the
|
|
4214
|
+
* classic suffixes (`b` blocks of 512, K/KiB, KB, M, G — powers of 1024
|
|
4215
|
+
* except the round-decimal `KB`/`MB`/`GB` spellings). Null means the value
|
|
4216
|
+
* is not a count these tools accept.
|
|
4217
|
+
*/
|
|
4218
|
+
function parseDumpCount(value: string): number | null {
|
|
4219
|
+
const match = /^(0[xX][0-9a-fA-F]+|0[0-7]*|[1-9][0-9]*)([bB]|[kKmMgGtT](?:i?[bB])?)?$/.exec(value);
|
|
4220
|
+
if (match === null) return null;
|
|
4221
|
+
const digits = match[1];
|
|
4222
|
+
const base = digits.startsWith('0x') || digits.startsWith('0X')
|
|
4223
|
+
? Number.parseInt(digits, 16)
|
|
4224
|
+
: /^0/.test(digits) ? Number.parseInt(digits, 8) : Number.parseInt(digits, 10);
|
|
4225
|
+
if (!Number.isSafeInteger(base) || base < 0) return null;
|
|
4226
|
+
if (match[2] === undefined || match[2] === '') return base;
|
|
4227
|
+
const scale: Record<string, number> = {
|
|
4228
|
+
b: 512, B: 512,
|
|
4229
|
+
k: 1024, K: 1024, KiB: 1024, kB: 1000, KB: 1000,
|
|
4230
|
+
m: 1024 ** 2, M: 1024 ** 2, MiB: 1024 ** 2, mB: 1000 ** 2, MB: 1000 ** 2,
|
|
4231
|
+
g: 1024 ** 3, G: 1024 ** 3, GiB: 1024 ** 3, gB: 1000 ** 3, GB: 1000 ** 3,
|
|
4232
|
+
t: 1024 ** 4, T: 1024 ** 4, TiB: 1024 ** 4, tB: 1000 ** 4, TB: 1000 ** 4,
|
|
4233
|
+
};
|
|
4234
|
+
const factor = scale[match[2]];
|
|
4235
|
+
if (factor === undefined) return null;
|
|
4236
|
+
const total = base * factor;
|
|
4237
|
+
return Number.isSafeInteger(total) ? total : null;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
/** Little-endian word; a short final chunk reads its missing bytes as zero. */
|
|
4241
|
+
function leWord(chunk: Uint8Array): number {
|
|
4242
|
+
return (chunk[0] ?? 0) | ((chunk[1] ?? 0) << 8);
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
/**
|
|
4246
|
+
* Shared row suppression: a formatted row equal to the one before it prints
|
|
4247
|
+
* as `*`, and runs of repeats collapse into that single marker. GNU od and
|
|
4248
|
+
* util-linux hexdump suppress the address together with the row, so callers
|
|
4249
|
+
* classify the body alone and print the marker bare.
|
|
4250
|
+
*/
|
|
4251
|
+
class RowDedup {
|
|
4252
|
+
private previous: string | null = null;
|
|
4253
|
+
private starred = false;
|
|
4254
|
+
|
|
4255
|
+
classify(row: string, verbose: boolean): 'print' | 'star' | 'skip' {
|
|
4256
|
+
if (verbose || row !== this.previous) {
|
|
4257
|
+
this.previous = row;
|
|
4258
|
+
this.starred = false;
|
|
4259
|
+
return 'print';
|
|
4260
|
+
}
|
|
4261
|
+
if (this.starred) return 'skip';
|
|
4262
|
+
this.starred = true;
|
|
4263
|
+
return 'star';
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
// ── od ──────────────────────────────────────────────────────────────────
|
|
4268
|
+
|
|
4269
|
+
type OdRadix = 'o' | 'd' | 'x' | 'n';
|
|
4270
|
+
|
|
4271
|
+
/**
|
|
4272
|
+
* Row addresses follow the radix (`0000000` octal or decimal, `000000`
|
|
4273
|
+
* lowercase hex). The closing total-length line keeps uppercase hex digits —
|
|
4274
|
+
* `00001B`, not `00001b` — matching od on this host byte for byte.
|
|
4275
|
+
*/
|
|
4276
|
+
function odAddress(radix: OdRadix, offset: number, final: boolean): string {
|
|
4277
|
+
if (radix === 'n') return '';
|
|
4278
|
+
if (radix === 'o') return offset.toString(8).padStart(7, '0');
|
|
4279
|
+
if (radix === 'd') return String(offset).padStart(7, '0');
|
|
4280
|
+
const digits = offset.toString(16);
|
|
4281
|
+
return (final ? digits.toUpperCase() : digits).padStart(6, '0');
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
/** `\0`-style escapes for `-tc`; other non-printables go out as `\NNN`. */
|
|
4285
|
+
const OD_CHAR_ESCAPES: Readonly<Record<number, string>> = {
|
|
4286
|
+
0: '\\0', 7: '\\a', 8: '\\b', 9: '\\t', 10: '\\n', 11: '\\v', 12: '\\f', 13: '\\r',
|
|
4287
|
+
};
|
|
4288
|
+
|
|
4289
|
+
interface OdTypeSpec {
|
|
4290
|
+
width: number;
|
|
4291
|
+
/** Rendered width of one item, used to align multiple -t types. */
|
|
4292
|
+
natural: number;
|
|
4293
|
+
render: (chunk: Uint8Array) => string;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
const OD_TYPES: Readonly<Record<string, OdTypeSpec>> = {
|
|
4297
|
+
c: {
|
|
4298
|
+
width: 1,
|
|
4299
|
+
natural: 3,
|
|
4300
|
+
render: (c) => {
|
|
4301
|
+
const escaped = OD_CHAR_ESCAPES[c[0]];
|
|
4302
|
+
if (escaped !== undefined) return escaped.padStart(3);
|
|
4303
|
+
if (c[0] >= 32 && c[0] < 127) return String.fromCharCode(c[0]).padStart(3);
|
|
4304
|
+
return c[0].toString(8).padStart(3, '0');
|
|
4305
|
+
},
|
|
4306
|
+
},
|
|
4307
|
+
d1: { width: 1, natural: 4, render: (c) => String((c[0] << 24) >> 24).padStart(4) },
|
|
4308
|
+
d2: { width: 2, natural: 6, render: (c) => { const v = leWord(c); return String(v >= 0x8000 ? v - 0x10000 : v).padStart(6); } },
|
|
4309
|
+
u1: { width: 1, natural: 3, render: (c) => String(c[0]).padStart(3) },
|
|
4310
|
+
u2: { width: 2, natural: 5, render: (c) => String(leWord(c)).padStart(5) },
|
|
4311
|
+
o1: { width: 1, natural: 3, render: (c) => c[0].toString(8).padStart(3, '0') },
|
|
4312
|
+
o2: { width: 2, natural: 6, render: (c) => leWord(c).toString(8).padStart(6, '0') },
|
|
4313
|
+
x1: { width: 1, natural: 2, render: (c) => c[0].toString(16).padStart(2, '0') },
|
|
4314
|
+
x2: { width: 2, natural: 4, render: (c) => leWord(c).toString(16).padStart(4, '0') },
|
|
4315
|
+
};
|
|
4316
|
+
|
|
4317
|
+
type OdArgs =
|
|
4318
|
+
| { error: string }
|
|
4319
|
+
| { radix: OdRadix; types: string[]; limit: number | undefined; verbose: boolean; files: string[] };
|
|
4320
|
+
|
|
4321
|
+
/**
|
|
4322
|
+
* Split a `-t` value into its concatenated specifications: a letter with an
|
|
4323
|
+
* optional size (`x1`, `d2`, `c`, …), so `-tx1c` yields x1 then c.
|
|
4324
|
+
*/
|
|
4325
|
+
function splitOdTypes(value: string): string[] {
|
|
4326
|
+
const specs: string[] = [];
|
|
4327
|
+
const pattern = /([xoducXODUC])([01248]?)/g;
|
|
4328
|
+
let consumed = '';
|
|
4329
|
+
for (const match of value.matchAll(pattern)) {
|
|
4330
|
+
consumed += match[0];
|
|
4331
|
+
const letter = match[1].toLowerCase();
|
|
4332
|
+
const size = match[2] === '' ? '' : match[2];
|
|
4333
|
+
if (letter === 'a') {
|
|
4334
|
+
specs.push('c');
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
specs.push(`${letter}${size}`);
|
|
4338
|
+
}
|
|
4339
|
+
return consumed === value ? specs : [value];
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
/** `-A[o|d|x|n]`, cumulative `-t<spec>`, `-N<count>`, `-v`. */
|
|
4343
|
+
function parseOdArgs(args: string[]): OdArgs {
|
|
4344
|
+
let radix: OdRadix = 'o';
|
|
4345
|
+
const types: string[] = [];
|
|
4346
|
+
let limit: number | undefined;
|
|
4347
|
+
let verbose = false;
|
|
4348
|
+
const files: string[] = [];
|
|
4349
|
+
for (let i = 0; i < args.length; i++) {
|
|
4350
|
+
const arg = args[i];
|
|
4351
|
+
if (arg === '--') { files.push(...args.slice(i + 1)); break; }
|
|
4352
|
+
if (!arg.startsWith('-') || arg === '-') { files.push(arg); continue; }
|
|
4353
|
+
const flag = arg[1];
|
|
4354
|
+
let value = arg.slice(2);
|
|
4355
|
+
if (flag === 'v' && value === '') { verbose = true; continue; }
|
|
4356
|
+
if (flag !== 'A' && flag !== 't' && flag !== 'N') {
|
|
4357
|
+
return { error: `od: invalid option -- '${flag}'` };
|
|
4358
|
+
}
|
|
4359
|
+
if (value === '') { value = args[i + 1]; if (value !== undefined) i++; }
|
|
4360
|
+
if (value === undefined || value === '') {
|
|
4361
|
+
return { error: `od: option requires an argument -- '${flag}'` };
|
|
4362
|
+
}
|
|
4363
|
+
if (flag === 'A') {
|
|
4364
|
+
// Exactly one radix letter per option (`-Aod` is not two glued flags);
|
|
4365
|
+
// repeated valid options are last-wins, like GNU od.
|
|
4366
|
+
if (value.length !== 1 || !'odxn'.includes(value)) {
|
|
4367
|
+
return { error: `od: Radix must be one of [o, d, x, n], got: ${value}` };
|
|
4206
4368
|
}
|
|
4207
|
-
|
|
4208
|
-
}
|
|
4369
|
+
radix = value as OdRadix;
|
|
4370
|
+
} else if (flag === 't') {
|
|
4371
|
+
// One -t may carry concatenated specifications (`-tx1c`); repeated
|
|
4372
|
+
// -t flags accumulate too. Both render in request order.
|
|
4373
|
+
for (const spec of splitOdTypes(value)) {
|
|
4374
|
+
if (!(spec in OD_TYPES)) return { error: `od: unsupported type specification '${value}'` };
|
|
4375
|
+
types.push(spec);
|
|
4376
|
+
}
|
|
4377
|
+
} else {
|
|
4378
|
+
const parsed = parseDumpCount(value);
|
|
4379
|
+
if (parsed === null || parsed < 0) {
|
|
4380
|
+
return { error: `od: invalid number of bytes '${value}'` };
|
|
4381
|
+
}
|
|
4382
|
+
limit = parsed;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
if (types.length === 0) types.push('o2');
|
|
4386
|
+
return { radix, types, limit, verbose, files };
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
/**
|
|
4390
|
+
* Sequential bytes for a dump tool: each operand in order, stdin once,
|
|
4391
|
+
* bounded range reads for files and bounded pulls for pipes. Only one
|
|
4392
|
+
* window of bytes is held at a time, so unbounded tools stream forever
|
|
4393
|
+
* instead of growing silently, and limits stop collection early.
|
|
4394
|
+
*/
|
|
4395
|
+
class DumpByteSource {
|
|
4396
|
+
private operands: (string | undefined)[];
|
|
4397
|
+
private operandIndex = 0;
|
|
4398
|
+
private remaining: number;
|
|
4399
|
+
private currentPath = '';
|
|
4400
|
+
private cursor = 0;
|
|
4401
|
+
private haveOpen = false;
|
|
4402
|
+
private stdinMode = false;
|
|
4403
|
+
private stdinUsed = false;
|
|
4404
|
+
private probedFirst = false;
|
|
4405
|
+
// Drained-string stdin is encoded once; pulls slice the encoded bytes so
|
|
4406
|
+
// multibyte input never advances past bytes it did not return.
|
|
4407
|
+
private stdinBytes: Uint8Array | null = null;
|
|
4408
|
+
private stdinCursor = 0;
|
|
4409
|
+
private stdinOverflow: Uint8Array[] = [];
|
|
4410
|
+
failures = 0;
|
|
4411
|
+
opened = 0;
|
|
4412
|
+
total = 0;
|
|
4413
|
+
|
|
4414
|
+
constructor(
|
|
4415
|
+
private ctx: Ctx,
|
|
4416
|
+
private label: string,
|
|
4417
|
+
files: string[],
|
|
4418
|
+
limit: number | undefined,
|
|
4419
|
+
) {
|
|
4420
|
+
this.operands = files.length > 0 ? files : [undefined];
|
|
4421
|
+
this.remaining = limit ?? Number.POSITIVE_INFINITY;
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
get failed(): boolean {
|
|
4425
|
+
return this.failures > 0;
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
/** Every named operand failed to open — distinct from successful empty. */
|
|
4429
|
+
get failedAll(): boolean {
|
|
4430
|
+
return this.operands.length > 0 && this.opened === 0 && this.failures > 0;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
private async openNextOperand(): Promise<boolean> {
|
|
4434
|
+
while (this.operandIndex < this.operands.length) {
|
|
4435
|
+
const file = this.operands[this.operandIndex++];
|
|
4436
|
+
try {
|
|
4437
|
+
if (file === undefined || file === '-') {
|
|
4438
|
+
if (this.stdinUsed) continue; // second '-' reads stdin already at EOF
|
|
4439
|
+
this.stdinMode = true;
|
|
4440
|
+
this.opened++; // an empty stdin still counts as successfully opened
|
|
4441
|
+
return true;
|
|
4442
|
+
}
|
|
4443
|
+
// Probe the file now so per-operand errors surface exactly once.
|
|
4444
|
+
// Named operands read through ctx.vfs — the mount-aware seam the
|
|
4445
|
+
// host handed the command — so /dev and other mounts resolve while
|
|
4446
|
+
// an embedder's credentialed view keeps its authorization.
|
|
4447
|
+
this.currentPath = absolutePath(this.ctx.cwd, file);
|
|
4448
|
+
this.ctx.vfs.readRange(this.currentPath, 0, 1);
|
|
4449
|
+
this.cursor = 0;
|
|
4450
|
+
this.haveOpen = true;
|
|
4451
|
+
this.opened++;
|
|
4452
|
+
return true;
|
|
4453
|
+
} catch (error) {
|
|
4454
|
+
this.ctx.stderr.write(`${this.label}: ${file}: ${fsErrorMessage(error)}\n`);
|
|
4455
|
+
this.failures++;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
return false;
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
private closeCurrent(): void {
|
|
4462
|
+
this.haveOpen = false;
|
|
4463
|
+
this.stdinMode = false;
|
|
4464
|
+
this.currentPath = '';
|
|
4465
|
+
this.cursor = 0;
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
/** One bounded pull; drained strings are encoded once and sliced by byte. */
|
|
4469
|
+
private async stdinPull(max: number): Promise<Uint8Array | null> {
|
|
4470
|
+
if (typeof this.ctx.stdin === 'string') {
|
|
4471
|
+
if (this.stdinBytes === null) this.stdinBytes = enc.encode(this.ctx.stdin);
|
|
4472
|
+
if (this.stdinCursor >= this.stdinBytes.length) return null;
|
|
4473
|
+
const end = Math.min(this.stdinCursor + max, this.stdinBytes.length);
|
|
4474
|
+
const chunk = this.stdinBytes.subarray(this.stdinCursor, end);
|
|
4475
|
+
this.stdinCursor = end;
|
|
4476
|
+
return chunk;
|
|
4477
|
+
}
|
|
4478
|
+
const reader = this.ctx.stdin as {
|
|
4479
|
+
read?: () => Promise<string | null>;
|
|
4480
|
+
readBytes?: (n: number) => Promise<Uint8Array | null>;
|
|
4481
|
+
};
|
|
4482
|
+
if (typeof reader?.read !== 'function') return null;
|
|
4483
|
+
// read/readAll-only embedders lose nothing: overflow bytes from a bounded
|
|
4484
|
+
// pull wait in stdinOverflow until the next one. A live stream hands back
|
|
4485
|
+
// its first available chunk — waiting to fill `max` would stall a sparse
|
|
4486
|
+
// producer that has written one byte and not the rest.
|
|
4487
|
+
let chunk: Uint8Array | null;
|
|
4488
|
+
if (this.stdinOverflow.length > 0) {
|
|
4489
|
+
chunk = this.stdinOverflow.shift() ?? null;
|
|
4490
|
+
} else if (reader.readBytes) {
|
|
4491
|
+
chunk = await reader.readBytes(Math.min(65536, max));
|
|
4492
|
+
} else {
|
|
4493
|
+
const text = await reader.read();
|
|
4494
|
+
chunk = text === null ? null : enc.encode(text);
|
|
4495
|
+
}
|
|
4496
|
+
if (chunk === null || chunk.length === 0) return null;
|
|
4497
|
+
if (chunk.length <= max) return chunk;
|
|
4498
|
+
this.stdinOverflow.unshift(chunk.subarray(max));
|
|
4499
|
+
return chunk.subarray(0, max);
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
/**
|
|
4503
|
+
* Up to `max` bytes, filling across chunks and operands. A row-oriented
|
|
4504
|
+
* caller wants the whole row before it formats anything, exactly as GNU od
|
|
4505
|
+
* fills its 16-byte buffer, so this waits for the count it asked for.
|
|
4506
|
+
*/
|
|
4507
|
+
async take(max: number): Promise<Uint8Array | null> {
|
|
4508
|
+
return this.collect(max, false);
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
/**
|
|
4512
|
+
* Up to `max` bytes, waiting only for the first ones to arrive. A caller
|
|
4513
|
+
* that formats whatever has landed uses this: file operands still answer
|
|
4514
|
+
* in bulk, while a live stream is never waited on for bytes a sparse
|
|
4515
|
+
* producer has not written yet.
|
|
4516
|
+
*/
|
|
4517
|
+
async takeReady(max: number): Promise<Uint8Array | null> {
|
|
4518
|
+
return this.collect(max, true);
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
private async collect(max: number, ready: boolean): Promise<Uint8Array | null> {
|
|
4522
|
+
if (!this.probedFirst) {
|
|
4523
|
+
// The first named operand must be attempted even under a zero limit,
|
|
4524
|
+
// so `-l0 /missing` reports the open error instead of succeeding.
|
|
4525
|
+
this.probedFirst = true;
|
|
4526
|
+
if (this.operands.length > 0 && !(await this.openNextOperand())) {
|
|
4527
|
+
return null;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
if (max <= 0) return new Uint8Array(0);
|
|
4531
|
+
const parts: Uint8Array[] = [];
|
|
4532
|
+
let got = 0;
|
|
4533
|
+
while (got < max) {
|
|
4534
|
+
if (this.remaining <= 0) break;
|
|
4535
|
+
if (!this.haveOpen && !this.stdinMode) {
|
|
4536
|
+
if (!(await this.openNextOperand())) break;
|
|
4537
|
+
}
|
|
4538
|
+
const want = Math.min(max - got, this.remaining, 65536);
|
|
4539
|
+
let chunk: Uint8Array | null;
|
|
4540
|
+
if (this.stdinMode) {
|
|
4541
|
+
chunk = await this.stdinPull(want);
|
|
4542
|
+
if (chunk === null || chunk.length === 0) {
|
|
4543
|
+
this.stdinUsed = true;
|
|
4544
|
+
this.closeCurrent();
|
|
4545
|
+
continue;
|
|
4546
|
+
}
|
|
4547
|
+
// A live stream hands back what it has: `takeReady` stops here so a
|
|
4548
|
+
// sparse producer keeps rendering, while `take` loops for the rest of
|
|
4549
|
+
// the row it was asked for.
|
|
4550
|
+
parts.push(chunk);
|
|
4551
|
+
got += chunk.length;
|
|
4552
|
+
this.total += chunk.length;
|
|
4553
|
+
this.remaining -= chunk.length;
|
|
4554
|
+
if (ready) break;
|
|
4555
|
+
continue;
|
|
4556
|
+
}
|
|
4557
|
+
// File operands keep filling: range reads are bulk and cost nothing
|
|
4558
|
+
// extra, and a block may span consecutive operands.
|
|
4559
|
+
chunk = this.ctx.vfs.readRange(this.currentPath, this.cursor, want);
|
|
4560
|
+
if (chunk.length === 0) {
|
|
4561
|
+
this.closeCurrent();
|
|
4562
|
+
continue;
|
|
4563
|
+
}
|
|
4564
|
+
this.cursor += chunk.length;
|
|
4565
|
+
const take = chunk.length <= want ? chunk : chunk.subarray(0, want);
|
|
4566
|
+
parts.push(take);
|
|
4567
|
+
got += take.length;
|
|
4568
|
+
this.total += take.length;
|
|
4569
|
+
this.remaining -= take.length;
|
|
4570
|
+
}
|
|
4571
|
+
if (parts.length === 0) return null;
|
|
4572
|
+
if (parts.length === 1) return parts[0];
|
|
4573
|
+
const out = new Uint8Array(got);
|
|
4574
|
+
let at = 0;
|
|
4575
|
+
for (const part of parts) {
|
|
4576
|
+
out.set(part, at);
|
|
4577
|
+
at += part.length;
|
|
4578
|
+
}
|
|
4579
|
+
return out;
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
function mkOd(): CmdFn {
|
|
4584
|
+
return async (ctx) => {
|
|
4585
|
+
const parsed = parseOdArgs(ctx.args);
|
|
4586
|
+
if ('error' in parsed) {
|
|
4587
|
+
ctx.stderr.write(`${parsed.error}\n`);
|
|
4588
|
+
return 1;
|
|
4589
|
+
}
|
|
4590
|
+
const src = new DumpByteSource(ctx, 'od', parsed.files, parsed.limit);
|
|
4591
|
+
const specs = parsed.types.map((name) => OD_TYPES[name]);
|
|
4592
|
+
// With several -t types every item shares one column width (the widest
|
|
4593
|
+
// natural width plus one); each type prints on its own continuation
|
|
4594
|
+
// line indented under the address — uutils od's grid on this host.
|
|
4595
|
+
const columnWidth = Math.max(...specs.map((spec) => spec.natural)) + 1;
|
|
4596
|
+
const renderRow = (row: Uint8Array): string[] => specs.map((spec) => {
|
|
4597
|
+
const items: string[] = [];
|
|
4598
|
+
for (let i = 0; i < row.length; i += spec.width) {
|
|
4599
|
+
items.push(spec.render(row.subarray(i, Math.min(i + spec.width, row.length))));
|
|
4600
|
+
}
|
|
4601
|
+
if (specs.length === 1) return items.join(' ');
|
|
4602
|
+
return items.map((item) => item.padStart(columnWidth)).join('');
|
|
4603
|
+
});
|
|
4604
|
+
const dedup = new RowDedup();
|
|
4605
|
+
while (true) {
|
|
4606
|
+
const row = await src.take(16);
|
|
4607
|
+
if (row === null || row.length === 0) break;
|
|
4608
|
+
const address = odAddress(parsed.radix, src.total - row.length, false);
|
|
4609
|
+
const lines = renderRow(row);
|
|
4610
|
+
const key = lines.join('\n');
|
|
4611
|
+
const indent = address === '' ? '' : ' '.repeat(7);
|
|
4612
|
+
const rendered = specs.length === 1
|
|
4613
|
+
? `${address} ${lines[0]}`
|
|
4614
|
+
: [`${address}${lines[0]}`, ...lines.slice(1).map((line) => `${indent}${line}`)].join('\n');
|
|
4615
|
+
switch (dedup.classify(key, parsed.verbose)) {
|
|
4616
|
+
case 'print':
|
|
4617
|
+
ctx.stdout.write(`${rendered}\n`);
|
|
4618
|
+
break;
|
|
4619
|
+
case 'star':
|
|
4620
|
+
ctx.stdout.write('*\n');
|
|
4621
|
+
break;
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
if (parsed.radix !== 'n') {
|
|
4625
|
+
if (src.failedAll) return 1;
|
|
4626
|
+
ctx.stdout.write(`${odAddress(parsed.radix, src.total, true)}\n`);
|
|
4627
|
+
}
|
|
4628
|
+
return src.failed ? 1 : 0;
|
|
4209
4629
|
};
|
|
4210
4630
|
}
|
|
4211
4631
|
|
|
4212
|
-
// ──
|
|
4632
|
+
// ── hexdump ─────────────────────────────────────────────────────────────
|
|
4213
4633
|
|
|
4634
|
+
interface HexdumpDirective {
|
|
4635
|
+
kind: 'byte' | 'addr';
|
|
4636
|
+
conv?: string;
|
|
4637
|
+
radix?: 'd' | 'o' | 'x';
|
|
4638
|
+
leftAlign: boolean;
|
|
4639
|
+
zeroPad: boolean;
|
|
4640
|
+
width: number | undefined;
|
|
4641
|
+
precision: number | undefined;
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4644
|
+
/** One quoted piece of an `-e` format: escaped text plus parsed directives. */
|
|
4645
|
+
interface HexdumpPiece {
|
|
4646
|
+
segments: (string | HexdumpDirective)[];
|
|
4647
|
+
count: number;
|
|
4648
|
+
size: number;
|
|
4649
|
+
consumes: boolean;
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
const HEXDUMP_WORD_SIZES: Readonly<Record<string, number>> = { '1': 1, '2': 2, '4': 4, C: 1 };
|
|
4653
|
+
/** Field widths an empty iteration pads to, mirroring util-linux. */
|
|
4654
|
+
const HEXDUMP_DIGIT_WIDTHS: Readonly<Record<string, { x: number; o: number; d: number }>> = {
|
|
4655
|
+
'1': { x: 2, o: 3, d: 3 },
|
|
4656
|
+
'2': { x: 4, o: 6, d: 5 },
|
|
4657
|
+
'4': { x: 8, o: 11, d: 10 },
|
|
4658
|
+
};
|
|
4659
|
+
|
|
4660
|
+
/** Shared empty unit: a conversion past end-of-input sees no bytes at all. */
|
|
4661
|
+
const HEXDUMP_EMPTY_UNIT = new Uint8Array(0);
|
|
4662
|
+
/** Source bytes fetched per pull while streaming `-e` blocks. */
|
|
4663
|
+
const HEXDUMP_PULL_BYTES = 4096;
|
|
4664
|
+
/** Digits an address directive can render: `Number.MAX_SAFE_INTEGER` in octal. */
|
|
4665
|
+
const HEXDUMP_MAX_ADDRESS_DIGITS = 20;
|
|
4214
4666
|
/**
|
|
4215
|
-
*
|
|
4216
|
-
*
|
|
4217
|
-
*
|
|
4218
|
-
*
|
|
4219
|
-
* Also resolves ctx.stdin from a stream object to a string.
|
|
4220
|
-
* The shell passes stdin as an object with .readAll() when piping,
|
|
4221
|
-
* but our commands expect a plain string.
|
|
4667
|
+
* Characters one `-e` block may render. A block is held twice while it is
|
|
4668
|
+
* classified — the line and its dedup key — so this ceiling bounds two
|
|
4669
|
+
* strings of ~2 MiB UTF-16 each, small beside a Durable Object's memory and
|
|
4670
|
+
* far above any format that dumps real data.
|
|
4222
4671
|
*/
|
|
4672
|
+
const HEXDUMP_MAX_BLOCK_CHARS = 1 << 20;
|
|
4673
|
+
|
|
4674
|
+
// util-linux rejects escaped delimiters inside -e units rather than
|
|
4675
|
+
// decoding them, so `\"` is deliberately absent here.
|
|
4676
|
+
const HEXDUMP_ESCAPES: Readonly<Record<string, string>> = {
|
|
4677
|
+
n: '\n', t: '\t', r: '\r', '\\': '\\', '0': '\0',
|
|
4678
|
+
};
|
|
4679
|
+
|
|
4680
|
+
function parseHexdumpDirectives(fmt: string): { segments: (string | HexdumpDirective)[] } | { error: string } {
|
|
4681
|
+
const segments: (string | HexdumpDirective)[] = [];
|
|
4682
|
+
let text = '';
|
|
4683
|
+
const flush = () => { if (text !== '') { segments.push(text); text = ''; } };
|
|
4684
|
+
const bad = (what: string) => ({ error: `hexdump: bad format {${what}}` });
|
|
4685
|
+
|
|
4686
|
+
for (let i = 0; i < fmt.length; i++) {
|
|
4687
|
+
const ch = fmt[i];
|
|
4688
|
+
if (ch === '\\') {
|
|
4689
|
+
const esc = fmt[++i];
|
|
4690
|
+
if (esc === undefined || !(esc in HEXDUMP_ESCAPES)) return bad(`\\${esc ?? ''}`);
|
|
4691
|
+
text += HEXDUMP_ESCAPES[esc];
|
|
4692
|
+
continue;
|
|
4693
|
+
}
|
|
4694
|
+
if (ch !== '%') { text += ch; continue; }
|
|
4695
|
+
flush();
|
|
4696
|
+
const directive: HexdumpDirective = { kind: 'byte', leftAlign: false, zeroPad: false, width: undefined, precision: undefined };
|
|
4697
|
+
let j = i + 1;
|
|
4698
|
+
while (fmt[j] === '-' || fmt[j] === '0') {
|
|
4699
|
+
if (fmt[j] === '-') directive.leftAlign = true; else directive.zeroPad = true;
|
|
4700
|
+
j++;
|
|
4701
|
+
}
|
|
4702
|
+
let digits = '';
|
|
4703
|
+
while (fmt[j] >= '0' && fmt[j] <= '9') digits += fmt[j++];
|
|
4704
|
+
if (digits !== '') {
|
|
4705
|
+
directive.width = Number(digits);
|
|
4706
|
+
// A width outside the safe-integer range can never render; reject the
|
|
4707
|
+
// format here rather than attempting an unbounded allocation later.
|
|
4708
|
+
if (!Number.isSafeInteger(directive.width)) return bad(`%${fmt.slice(i + 1, j + 1)}`);
|
|
4709
|
+
}
|
|
4710
|
+
if (fmt[j] === '.') {
|
|
4711
|
+
let prec = '';
|
|
4712
|
+
j++;
|
|
4713
|
+
while (fmt[j] >= '0' && fmt[j] <= '9') prec += fmt[j++];
|
|
4714
|
+
directive.precision = prec === '' ? 0 : Number(prec);
|
|
4715
|
+
if (!Number.isSafeInteger(directive.precision)) return bad(`%${fmt.slice(i + 1, j + 1)}`);
|
|
4716
|
+
}
|
|
4717
|
+
if (fmt[j] === '_' && fmt[j + 1] === 'a' && 'dxo'.includes(fmt[j + 2])) {
|
|
4718
|
+
directive.kind = 'addr';
|
|
4719
|
+
directive.radix = fmt[j + 2] as 'd' | 'o' | 'x';
|
|
4720
|
+
i = j + 2;
|
|
4721
|
+
} else if (j < fmt.length && 'xXduoc'.includes(fmt[j])) {
|
|
4722
|
+
directive.conv = fmt[j];
|
|
4723
|
+
i = j;
|
|
4724
|
+
} else {
|
|
4725
|
+
return bad(`%${fmt.slice(i + 1, j + 1)}`);
|
|
4726
|
+
}
|
|
4727
|
+
segments.push(directive);
|
|
4728
|
+
}
|
|
4729
|
+
flush();
|
|
4730
|
+
return { segments };
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
function parseHexdumpPieces(value: string): { pieces: HexdumpPiece[] } | { error: string } {
|
|
4734
|
+
const pieces: HexdumpPiece[] = [];
|
|
4735
|
+
let pending: { count: number; size: number } | null = null;
|
|
4736
|
+
let i = 0;
|
|
4737
|
+
while (i < value.length) {
|
|
4738
|
+
const ch = value[i];
|
|
4739
|
+
if (ch === ' ' || ch === '\t') { i++; continue; }
|
|
4740
|
+
if (ch >= '0' && ch <= '9') {
|
|
4741
|
+
let digits = '';
|
|
4742
|
+
while (i < value.length && value[i] >= '0' && value[i] <= '9') digits += value[i++];
|
|
4743
|
+
if (value[i] !== '/') return { error: `hexdump: bad format {${value}}` };
|
|
4744
|
+
const sizeChar = value[i + 1] ?? '';
|
|
4745
|
+
const size = HEXDUMP_WORD_SIZES[sizeChar];
|
|
4746
|
+
if (size === undefined) return { error: `hexdump: bad format {${digits}/${sizeChar}}` };
|
|
4747
|
+
const count = Number(digits);
|
|
4748
|
+
if (!Number.isSafeInteger(count) || count <= 0) {
|
|
4749
|
+
return { error: `hexdump: bad format {${digits}/${sizeChar}}` };
|
|
4750
|
+
}
|
|
4751
|
+
pending = { count, size };
|
|
4752
|
+
i += 2;
|
|
4753
|
+
continue;
|
|
4754
|
+
}
|
|
4755
|
+
if (ch !== '"' && ch !== "'") return { error: `hexdump: bad format {${value.slice(i)}}` };
|
|
4756
|
+
// util-linux refuses an escaped quote inside a unit instead of decoding
|
|
4757
|
+
// it, and names the whole specification when it does.
|
|
4758
|
+
let close = -1;
|
|
4759
|
+
for (let k = i + 1; k < value.length; k++) {
|
|
4760
|
+
const c = value[k];
|
|
4761
|
+
if (c === '\\') {
|
|
4762
|
+
if (value[k + 1] === ch) { close = -2; break; }
|
|
4763
|
+
k++;
|
|
4764
|
+
continue;
|
|
4765
|
+
}
|
|
4766
|
+
if (c === ch) { close = k; break; }
|
|
4767
|
+
}
|
|
4768
|
+
if (close < 0) return { error: `hexdump: bad format {${value}}` };
|
|
4769
|
+
const parsed = parseHexdumpDirectives(value.slice(i + 1, close));
|
|
4770
|
+
if ('error' in parsed) return parsed;
|
|
4771
|
+
const conversions = parsed.segments.filter(
|
|
4772
|
+
(segment) => typeof segment !== 'string' && segment.kind === 'byte',
|
|
4773
|
+
).length;
|
|
4774
|
+
// util-linux refuses a byte count feeding more than one conversion.
|
|
4775
|
+
if (conversions > 1) {
|
|
4776
|
+
return { error: 'hexdump: byte count with multiple conversion characters' };
|
|
4777
|
+
}
|
|
4778
|
+
// %c defaults to one byte and only accepts one; other conversions
|
|
4779
|
+
// default to four.
|
|
4780
|
+
const soleConv = conversions === 1
|
|
4781
|
+
? (parsed.segments.find(
|
|
4782
|
+
(segment) => typeof segment !== 'string' && segment.kind === 'byte',
|
|
4783
|
+
) as HexdumpDirective).conv
|
|
4784
|
+
: undefined;
|
|
4785
|
+
if (soleConv === 'c' && pending !== null && pending.size !== 1) {
|
|
4786
|
+
return { error: 'hexdump: bad byte count for conversion character c' };
|
|
4787
|
+
}
|
|
4788
|
+
pieces.push({
|
|
4789
|
+
segments: parsed.segments,
|
|
4790
|
+
count: pending?.count ?? 1,
|
|
4791
|
+
size: pending?.size ?? (soleConv === 'c' ? 1 : 4),
|
|
4792
|
+
consumes: conversions > 0,
|
|
4793
|
+
});
|
|
4794
|
+
pending = null;
|
|
4795
|
+
i = close + 1;
|
|
4796
|
+
}
|
|
4797
|
+
if (pending !== null) return { error: `hexdump: bad format {${value}}` };
|
|
4798
|
+
return { pieces };
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
function hexdumpFormatNumber(directive: HexdumpDirective, digits: string): string {
|
|
4802
|
+
// fprintf rules: precision pads the magnitude with zeros, a sign always
|
|
4803
|
+
// sits in front of that padding, '-' alignment overrides '0', and an
|
|
4804
|
+
// explicit precision disables '0' field padding entirely.
|
|
4805
|
+
let sign = '';
|
|
4806
|
+
let magnitude = digits;
|
|
4807
|
+
if (magnitude.startsWith('-')) {
|
|
4808
|
+
sign = '-';
|
|
4809
|
+
magnitude = magnitude.slice(1);
|
|
4810
|
+
}
|
|
4811
|
+
if (directive.precision !== undefined) magnitude = magnitude.padStart(directive.precision, '0');
|
|
4812
|
+
const width = directive.width ?? 0;
|
|
4813
|
+
const value = sign + magnitude;
|
|
4814
|
+
if (directive.leftAlign) return value.padEnd(width);
|
|
4815
|
+
if (directive.zeroPad && directive.precision === undefined && width > 0) {
|
|
4816
|
+
return sign + magnitude.padStart(width - sign.length, '0');
|
|
4817
|
+
}
|
|
4818
|
+
return value.padStart(width);
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
/** Field width an iteration reserves, so missing ones pad like util-linux. */
|
|
4822
|
+
function hexdumpFieldWidth(directive: HexdumpDirective, size: number): number {
|
|
4823
|
+
if (directive.kind === 'addr') return directive.width ?? 0;
|
|
4824
|
+
if (directive.conv === 'c') return directive.width ?? 1;
|
|
4825
|
+
const family = directive.conv === 'x' || directive.conv === 'X'
|
|
4826
|
+
? 'x'
|
|
4827
|
+
: directive.conv === 'o' ? 'o' : 'd';
|
|
4828
|
+
return directive.width ?? HEXDUMP_DIGIT_WIDTHS[String(size)][family];
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4223
4831
|
/**
|
|
4224
|
-
*
|
|
4225
|
-
*
|
|
4226
|
-
*
|
|
4832
|
+
* Render one `-e` block straight off the source, one unit at a time. A
|
|
4833
|
+
* directive past the end of input renders as field-width spaces rather than
|
|
4834
|
+
* dropping its slot, and address directives report the offset of the next
|
|
4835
|
+
* byte to display. The dedup key repeats all of it except addresses, so
|
|
4836
|
+
* repeat suppression ignores where each block sits.
|
|
4837
|
+
*
|
|
4838
|
+
* util-linux's nospace rule: a unit repeated more than once drops the single
|
|
4839
|
+
* trailing whitespace character of its own format text on its LAST
|
|
4840
|
+
* repetition, EOF padding included. Exactly that one character goes, which
|
|
4841
|
+
* is why `3/1 "%02x "` keeps one of its two spaces, `2/1 "%02x\t"` keeps
|
|
4842
|
+
* the padding that follows the dropped tab, and a unit repeated once keeps
|
|
4843
|
+
* its spacing verbatim.
|
|
4227
4844
|
*
|
|
4228
|
-
*
|
|
4229
|
-
*
|
|
4230
|
-
*
|
|
4231
|
-
*
|
|
4232
|
-
*
|
|
4233
|
-
*
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4845
|
+
* Input arrives unit-sized: nothing collects count*size bytes however large
|
|
4846
|
+
* the repetition count is, and parseHexdumpArgs has already refused any
|
|
4847
|
+
* format whose block could outgrow {@link HEXDUMP_MAX_BLOCK_CHARS}.
|
|
4848
|
+
*
|
|
4849
|
+
* Returns the rendered line, its address-free key, and the source bytes the
|
|
4850
|
+
* block consumed; null once a block consumes nothing, i.e. true end.
|
|
4851
|
+
*/
|
|
4852
|
+
async function hexdumpRenderBlock(
|
|
4853
|
+
pieces: HexdumpPiece[],
|
|
4854
|
+
pullUnit: (size: number) => Promise<Uint8Array>,
|
|
4855
|
+
blockStart: number,
|
|
4856
|
+
): Promise<{ line: string; key: string; consumed: number } | null> {
|
|
4857
|
+
let line = '';
|
|
4858
|
+
let key = '';
|
|
4859
|
+
let pos = 0;
|
|
4860
|
+
let consumed = 0;
|
|
4861
|
+
let ended = false;
|
|
4862
|
+
for (const piece of pieces) {
|
|
4863
|
+
const iterations = piece.consumes ? piece.count : 1;
|
|
4864
|
+
const tail = piece.segments[piece.segments.length - 1];
|
|
4865
|
+
const dropsTailSpace = iterations > 1
|
|
4866
|
+
&& typeof tail === 'string'
|
|
4867
|
+
&& /[ \t\n\r\v\f]$/.test(tail);
|
|
4868
|
+
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
4869
|
+
const lastIteration = iteration === iterations - 1;
|
|
4870
|
+
let unit: Uint8Array = HEXDUMP_EMPTY_UNIT;
|
|
4871
|
+
if (piece.consumes && !ended) {
|
|
4872
|
+
unit = await pullUnit(piece.size);
|
|
4873
|
+
if (unit.length < piece.size) ended = true;
|
|
4874
|
+
}
|
|
4875
|
+
const missing = piece.consumes && unit.length === 0;
|
|
4876
|
+
// Literals always render; only byte conversions pad when input ran
|
|
4877
|
+
// out, so a trailing literal still reaches the line on short blocks.
|
|
4878
|
+
for (let index = 0; index < piece.segments.length; index++) {
|
|
4879
|
+
const segment = piece.segments[index];
|
|
4880
|
+
if (typeof segment === 'string') {
|
|
4881
|
+
const text = dropsTailSpace && lastIteration && index === piece.segments.length - 1
|
|
4882
|
+
? segment.slice(0, -1)
|
|
4883
|
+
: segment;
|
|
4884
|
+
line += text;
|
|
4885
|
+
key += text;
|
|
4886
|
+
continue;
|
|
4887
|
+
}
|
|
4888
|
+
if (segment.kind === 'addr') {
|
|
4889
|
+
const shown = blockStart + pos;
|
|
4890
|
+
const digits = segment.radix === 'd'
|
|
4891
|
+
? String(shown)
|
|
4892
|
+
: shown.toString(segment.radix === 'o' ? 8 : 16);
|
|
4893
|
+
line += hexdumpFormatNumber(segment, digits);
|
|
4894
|
+
continue;
|
|
4895
|
+
}
|
|
4896
|
+
if (missing) {
|
|
4897
|
+
const pad = ' '.repeat(hexdumpFieldWidth(segment, piece.size));
|
|
4898
|
+
line += pad;
|
|
4899
|
+
key += pad;
|
|
4900
|
+
continue;
|
|
4901
|
+
}
|
|
4902
|
+
let value = 0;
|
|
4903
|
+
for (let b = piece.size - 1; b >= 0; b--) value = value * 256 + (unit[b] ?? 0);
|
|
4904
|
+
pos += piece.size;
|
|
4905
|
+
if (segment.conv === 'c') {
|
|
4906
|
+
const text = hexdumpFormatNumber(segment, String.fromCharCode(value & 0xff));
|
|
4907
|
+
line += text;
|
|
4908
|
+
key += text;
|
|
4909
|
+
continue;
|
|
4910
|
+
}
|
|
4911
|
+
let digits: string;
|
|
4912
|
+
if (segment.conv === 'd') {
|
|
4913
|
+
const signBit = 256 ** piece.size / 2;
|
|
4914
|
+
digits = String(value >= signBit ? value - signBit * 2 : value);
|
|
4915
|
+
} else if (segment.conv === 'u') {
|
|
4916
|
+
digits = String(value);
|
|
4917
|
+
} else if (segment.conv === 'o') {
|
|
4918
|
+
digits = value.toString(8);
|
|
4919
|
+
} else {
|
|
4920
|
+
digits = value.toString(16);
|
|
4921
|
+
if (segment.conv === 'X') digits = digits.toUpperCase();
|
|
4922
|
+
}
|
|
4923
|
+
const text = hexdumpFormatNumber(segment, digits);
|
|
4924
|
+
line += text;
|
|
4925
|
+
key += text;
|
|
4926
|
+
}
|
|
4927
|
+
consumed += unit.length;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
if (consumed === 0) return null;
|
|
4931
|
+
return { line, key, consumed };
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
/** Body columns for the fixed modes; the caller prefixes the address. */
|
|
4935
|
+
function hexdumpFixedBody(mode: 'default' | 'x' | 'd' | 'o' | 'C', row: Uint8Array): { body: string; bar: string | null } {
|
|
4936
|
+
if (mode === 'C') {
|
|
4937
|
+
const group1 = Array.from(row.subarray(0, 8), (b) => b.toString(16).padStart(2, '0'));
|
|
4938
|
+
const group2 = Array.from(row.subarray(8), (b) => b.toString(16).padStart(2, '0'));
|
|
4939
|
+
const columns = group1.join(' ') + (group2.length > 0 ? ' ' + group2.join(' ') : '');
|
|
4940
|
+
const bar = Array.from(row, (b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : '.')).join('');
|
|
4941
|
+
return { body: columns.padEnd(50), bar };
|
|
4942
|
+
}
|
|
4943
|
+
if (mode === 'default') {
|
|
4944
|
+
const words: string[] = [];
|
|
4945
|
+
for (let i = 0; i < row.length; i += 2) {
|
|
4946
|
+
words.push(leWord(row.subarray(i)).toString(16).padStart(4, '0'));
|
|
4947
|
+
}
|
|
4948
|
+
return { body: words.join(' ').padEnd(39), bar: null };
|
|
4949
|
+
}
|
|
4950
|
+
const slots: string[] = [];
|
|
4951
|
+
for (let i = 0; i < 16; i += 2) {
|
|
4952
|
+
const chunk = row.subarray(i);
|
|
4953
|
+
if (chunk.length === 0) { slots.push(' '.repeat(8)); continue; }
|
|
4954
|
+
const rendered = mode === 'x'
|
|
4955
|
+
? leWord(chunk).toString(16).padStart(4, '0')
|
|
4956
|
+
: mode === 'd'
|
|
4957
|
+
? String(leWord(chunk)).padStart(5, '0')
|
|
4958
|
+
: leWord(chunk).toString(8).padStart(6, '0');
|
|
4959
|
+
slots.push(rendered.padStart(i === 0 ? 7 : 8));
|
|
4960
|
+
}
|
|
4961
|
+
return { body: slots.join(''), bar: null };
|
|
4962
|
+
}
|
|
4963
|
+
|
|
4964
|
+
type HexdumpArgs =
|
|
4965
|
+
| { error: string }
|
|
4966
|
+
| {
|
|
4967
|
+
mode: 'default' | 'x' | 'd' | 'o' | 'C';
|
|
4968
|
+
pieces: HexdumpPiece[] | null;
|
|
4969
|
+
length: number | undefined;
|
|
4970
|
+
verbose: boolean;
|
|
4971
|
+
files: string[];
|
|
4972
|
+
};
|
|
4973
|
+
|
|
4974
|
+
/** Format flags override each other (last wins); `-e` replaces them all. */
|
|
4975
|
+
function parseHexdumpArgs(args: string[]): HexdumpArgs {
|
|
4976
|
+
let mode: 'default' | 'x' | 'd' | 'o' | 'C' = 'default';
|
|
4977
|
+
let pieces: HexdumpPiece[] | null = null;
|
|
4978
|
+
let length: number | undefined;
|
|
4979
|
+
let verbose = false;
|
|
4980
|
+
const files: string[] = [];
|
|
4981
|
+
|
|
4982
|
+
for (let i = 0; i < args.length; i++) {
|
|
4983
|
+
const arg = args[i];
|
|
4984
|
+
if (arg === '--') { files.push(...args.slice(i + 1)); break; }
|
|
4985
|
+
if (!arg.startsWith('-') || arg === '-') { files.push(arg); continue; }
|
|
4986
|
+
const flag = arg[1];
|
|
4987
|
+
const inlineValue = arg.slice(2);
|
|
4988
|
+
if (flag === 'C' || flag === 'x' || flag === 'd' || flag === 'o') {
|
|
4989
|
+
if (inlineValue !== '') return { error: `hexdump: invalid option -- '${flag}'` };
|
|
4990
|
+
mode = flag;
|
|
4991
|
+
continue;
|
|
4992
|
+
}
|
|
4993
|
+
if (flag === 'v' && inlineValue === '') { verbose = true; continue; }
|
|
4994
|
+
if (flag !== 'e' && flag !== 'n') {
|
|
4995
|
+
return { error: `hexdump: invalid option -- '${flag}'` };
|
|
4996
|
+
}
|
|
4997
|
+
const value = inlineValue || args[++i];
|
|
4998
|
+
if (value === undefined || value === '') {
|
|
4999
|
+
return { error: `hexdump: option requires an argument -- '${flag}'` };
|
|
5000
|
+
}
|
|
5001
|
+
if (flag === 'n') {
|
|
5002
|
+
const parsed = parseDumpCount(value);
|
|
5003
|
+
if (parsed === null || parsed < 0) {
|
|
5004
|
+
return { error: `hexdump: invalid length '${value}'` };
|
|
5005
|
+
}
|
|
5006
|
+
length = parsed;
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
if (pieces !== null) return { error: 'hexdump: only one -e format is supported' };
|
|
5010
|
+
const parsed = parseHexdumpPieces(value);
|
|
5011
|
+
if ('error' in parsed) return parsed;
|
|
5012
|
+
if (!parsed.pieces.some((piece) => piece.consumes)) {
|
|
5013
|
+
return { error: `hexdump: bad format {${value}}` };
|
|
5014
|
+
}
|
|
5015
|
+
// The block is held twice while it is classified, so a format whose
|
|
5016
|
+
// rendering cannot fit is refused here — before a byte is ever read.
|
|
5017
|
+
let blockChars = 0;
|
|
5018
|
+
for (const piece of parsed.pieces) {
|
|
5019
|
+
let perIteration = 0;
|
|
5020
|
+
for (const segment of piece.segments) {
|
|
5021
|
+
if (typeof segment === 'string') { perIteration += segment.length; continue; }
|
|
5022
|
+
if (segment.kind === 'addr') {
|
|
5023
|
+
perIteration += Math.max(segment.width ?? 0, HEXDUMP_MAX_ADDRESS_DIGITS);
|
|
5024
|
+
continue;
|
|
5025
|
+
}
|
|
5026
|
+
// A narrow field cannot shrink a value: `%1u` over four bytes still
|
|
5027
|
+
// renders ten characters. Size each conversion by the widest of its
|
|
5028
|
+
// field width, its precision plus a sign, and the natural maximum
|
|
5029
|
+
// for its byte size.
|
|
5030
|
+
const digits = HEXDUMP_DIGIT_WIDTHS[String(piece.size)];
|
|
5031
|
+
const natural = segment.conv === 'c'
|
|
5032
|
+
? 1
|
|
5033
|
+
: segment.conv === 'd'
|
|
5034
|
+
? digits.d + 1
|
|
5035
|
+
: segment.conv === 'u'
|
|
5036
|
+
? digits.d
|
|
5037
|
+
: segment.conv === 'o'
|
|
5038
|
+
? digits.o
|
|
5039
|
+
: digits.x;
|
|
5040
|
+
perIteration += Math.max(segment.width ?? 0, natural, (segment.precision ?? 0) + 1);
|
|
5041
|
+
}
|
|
5042
|
+
blockChars += (piece.consumes ? piece.count : 1) * perIteration;
|
|
5043
|
+
}
|
|
5044
|
+
if (blockChars > HEXDUMP_MAX_BLOCK_CHARS) {
|
|
5045
|
+
return {
|
|
5046
|
+
error: `hexdump: format renders ${blockChars} characters per block, over the ${HEXDUMP_MAX_BLOCK_CHARS} limit`,
|
|
5047
|
+
};
|
|
5048
|
+
}
|
|
5049
|
+
pieces = parsed.pieces;
|
|
5050
|
+
}
|
|
5051
|
+
return { mode, pieces, length, verbose, files };
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
function mkHexdump(): CmdFn {
|
|
5055
|
+
return async (ctx) => {
|
|
5056
|
+
const parsed = parseHexdumpArgs(ctx.args);
|
|
5057
|
+
if ('error' in parsed) {
|
|
5058
|
+
ctx.stderr.write(`${parsed.error}\n`);
|
|
5059
|
+
return 1;
|
|
5060
|
+
}
|
|
5061
|
+
const src = new DumpByteSource(ctx, 'hexdump', parsed.files, parsed.length);
|
|
5062
|
+
const dedup = new RowDedup();
|
|
5063
|
+
if (parsed.pieces !== null) {
|
|
5064
|
+
const lastPiece = parsed.pieces[parsed.pieces.length - 1];
|
|
5065
|
+
const lastSegment = lastPiece.segments[lastPiece.segments.length - 1];
|
|
5066
|
+
// Repeat suppression needs line boundaries; free-form formats emit
|
|
5067
|
+
// the whole stream, which is what -v spells on util-linux.
|
|
5068
|
+
const lineStructured = typeof lastSegment === 'string' && lastSegment.endsWith('\n');
|
|
5069
|
+
// Units arrive from a small stash fed well ahead of demand, so
|
|
5070
|
+
// per-unit pulls amortize into large source reads without ever
|
|
5071
|
+
// holding count*size bytes for a block.
|
|
5072
|
+
let pending = HEXDUMP_EMPTY_UNIT;
|
|
5073
|
+
const pullUnit = async (size: number): Promise<Uint8Array> => {
|
|
5074
|
+
while (pending.length < size) {
|
|
5075
|
+
// Ready reads: a file answers the whole block in one range read,
|
|
5076
|
+
// and a live producer answers with whatever it has already written.
|
|
5077
|
+
const chunk = await src.takeReady(Math.max(size - pending.length, HEXDUMP_PULL_BYTES));
|
|
5078
|
+
if (chunk === null || chunk.length === 0) break;
|
|
5079
|
+
const merged = new Uint8Array(pending.length + chunk.length);
|
|
5080
|
+
merged.set(pending);
|
|
5081
|
+
merged.set(chunk, pending.length);
|
|
5082
|
+
pending = merged;
|
|
5083
|
+
}
|
|
5084
|
+
const unit = pending.subarray(0, size);
|
|
5085
|
+
pending = pending.subarray(unit.length);
|
|
5086
|
+
return unit;
|
|
5087
|
+
};
|
|
5088
|
+
let offset = 0;
|
|
5089
|
+
while (true) {
|
|
5090
|
+
const block = await hexdumpRenderBlock(parsed.pieces, pullUnit, offset);
|
|
5091
|
+
if (block === null) break;
|
|
5092
|
+
offset += block.consumed;
|
|
5093
|
+
if (!lineStructured) { ctx.stdout.write(block.line); continue; }
|
|
5094
|
+
switch (dedup.classify(block.key, parsed.verbose)) {
|
|
5095
|
+
case 'print': ctx.stdout.write(block.line); break;
|
|
5096
|
+
case 'star': ctx.stdout.write('*\n'); break;
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
if (src.failedAll) ctx.stderr.write('hexdump: all input file arguments failed\n');
|
|
5100
|
+
return src.failed ? 1 : 0;
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
const wide = parsed.mode === 'C';
|
|
5104
|
+
while (true) {
|
|
5105
|
+
const row = await src.take(16);
|
|
5106
|
+
if (row === null || row.length === 0) break;
|
|
5107
|
+
const { body, bar } = hexdumpFixedBody(parsed.mode, row);
|
|
5108
|
+
const address = (src.total - row.length).toString(16).padStart(wide ? 8 : 7, '0');
|
|
5109
|
+
switch (dedup.classify(body, parsed.verbose)) {
|
|
5110
|
+
case 'print':
|
|
5111
|
+
ctx.stdout.write(wide ? `${address} ${body}|${bar}|\n` : `${address} ${body}\n`);
|
|
5112
|
+
break;
|
|
5113
|
+
case 'star':
|
|
5114
|
+
ctx.stdout.write('*\n');
|
|
5115
|
+
break;
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
if (src.failedAll) ctx.stderr.write('hexdump: all input file arguments failed\n');
|
|
5119
|
+
if (src.total > 0) {
|
|
5120
|
+
ctx.stdout.write(`${src.total.toString(16).padStart(wide ? 8 : 7, '0')}\n`);
|
|
5121
|
+
}
|
|
5122
|
+
return src.failed ? 1 : 0;
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5126
|
+
// ── xxd ─────────────────────────────────────────────────────────────────
|
|
5127
|
+
|
|
5128
|
+
/**
|
|
5129
|
+
* `xxd [FILE [-] [OUTFILE]]` — pipelines are xxd's primary use, so stdin is
|
|
5130
|
+
* read when no input operand is given or `-` names it. A second positional
|
|
5131
|
+
* operand receives the dump as a file, like real xxd. `-l N` limits the dump
|
|
5132
|
+
* (decimal, 0x hex, leading-zero octal, with count suffixes); `-p` emits
|
|
5133
|
+
* continuous hex in bounded 30-byte rows. The default row layout predates
|
|
5134
|
+
* this fix and is preserved verbatim.
|
|
4238
5135
|
*/
|
|
5136
|
+
function mkXxd(): CmdFn {
|
|
5137
|
+
return async (ctx) => {
|
|
5138
|
+
let plain = false;
|
|
5139
|
+
let limit: number | undefined;
|
|
5140
|
+
const operands: string[] = [];
|
|
5141
|
+
for (let i = 0; i < ctx.args.length; i++) {
|
|
5142
|
+
const arg = ctx.args[i];
|
|
5143
|
+
if (arg === '-p') { plain = true; continue; }
|
|
5144
|
+
if (arg === '-' || !arg.startsWith('-')) {
|
|
5145
|
+
if (operands.length >= 2) {
|
|
5146
|
+
ctx.stderr.write(`xxd: extra operand '${arg}'\n`);
|
|
5147
|
+
return 1;
|
|
5148
|
+
}
|
|
5149
|
+
operands.push(arg);
|
|
5150
|
+
continue;
|
|
5151
|
+
}
|
|
5152
|
+
if (arg === '-l' || arg.startsWith('-l')) {
|
|
5153
|
+
const value = arg === '-l' ? ctx.args[++i] : arg.slice(2);
|
|
5154
|
+
const parsed = value === undefined ? null : parseDumpCount(value);
|
|
5155
|
+
if (parsed === null || parsed < 0) {
|
|
5156
|
+
ctx.stderr.write(`xxd: invalid length value '${value ?? ''}'\n`);
|
|
5157
|
+
return 1;
|
|
5158
|
+
}
|
|
5159
|
+
limit = parsed;
|
|
5160
|
+
continue;
|
|
5161
|
+
}
|
|
5162
|
+
ctx.stderr.write(`xxd: invalid option -- '${arg.replace(/^-+/, '')}'\n`);
|
|
5163
|
+
return 1;
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
// Prime the source FIRST: pull the initial window (surfacing any open
|
|
5167
|
+
// error) before the output file exists to truncate.
|
|
5168
|
+
const rowSize = plain ? 30 : 16;
|
|
5169
|
+
const src = new DumpByteSource(ctx, 'xxd', operands.slice(0, 1), limit);
|
|
5170
|
+
const firstWindow = await src.take(rowSize);
|
|
5171
|
+
if (src.failedAll || (src.failed && src.opened === 0)) return 1;
|
|
5172
|
+
|
|
5173
|
+
// A second operand names the output file; `-` there means stdout. It
|
|
5174
|
+
// routes through ctx.vfs like every other named path, so dumps may land
|
|
5175
|
+
// on devices and mounts as they do on Unix.
|
|
5176
|
+
const output = operands[1];
|
|
5177
|
+
const outAbs = output !== undefined && output !== '-' ? absolutePath(ctx.cwd, output) : null;
|
|
5178
|
+
|
|
5179
|
+
let offset = 0;
|
|
5180
|
+
let fileOffset = 0;
|
|
5181
|
+
let pending: string[] = [];
|
|
5182
|
+
let pendingBytes = 0;
|
|
5183
|
+
let writeFailed = false;
|
|
5184
|
+
const flush = () => {
|
|
5185
|
+
if (pending.length === 0 || writeFailed || outAbs === null) return;
|
|
5186
|
+
try {
|
|
5187
|
+
ctx.vfs.writeRange(outAbs, fileOffset, encode(pending.join('')));
|
|
5188
|
+
} catch (error) {
|
|
5189
|
+
ctx.stderr.write(`xxd: ${output}: ${fsErrorMessage(error)}\n`);
|
|
5190
|
+
writeFailed = true;
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
5193
|
+
fileOffset += pendingBytes;
|
|
5194
|
+
pending = [];
|
|
5195
|
+
pendingBytes = 0;
|
|
5196
|
+
};
|
|
5197
|
+
|
|
5198
|
+
const renderWindow = (rowOffset: number, window: Uint8Array): string => {
|
|
5199
|
+
if (plain) {
|
|
5200
|
+
let line = '';
|
|
5201
|
+
for (const byte of window) line += byte.toString(16).padStart(2, '0');
|
|
5202
|
+
return `${line}\n`;
|
|
5203
|
+
}
|
|
5204
|
+
const pairs = Array.from(window, (b) => b.toString(16).padStart(2, '0')).join(' ');
|
|
5205
|
+
const ascii = Array.from(window, (b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : '.')).join('');
|
|
5206
|
+
return `${rowOffset.toString(16).padStart(8, '0')}: ${pairs.padEnd(48)} ${ascii}\n`;
|
|
5207
|
+
};
|
|
5208
|
+
|
|
5209
|
+
if (outAbs !== null) {
|
|
5210
|
+
// Input proved readable above, so truncating here cannot destroy data
|
|
5211
|
+
// on a failed dump.
|
|
5212
|
+
try {
|
|
5213
|
+
ctx.vfs.writeFile(outAbs, '');
|
|
5214
|
+
} catch (error) {
|
|
5215
|
+
ctx.stderr.write(`xxd: ${output}: ${fsErrorMessage(error)}\n`);
|
|
5216
|
+
return 1;
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
let window = firstWindow;
|
|
5221
|
+
while (window !== null && window.length > 0 && !writeFailed) {
|
|
5222
|
+
const text = renderWindow(offset, window);
|
|
5223
|
+
offset += window.length;
|
|
5224
|
+
if (outAbs !== null) {
|
|
5225
|
+
pending.push(text);
|
|
5226
|
+
pendingBytes += text.length;
|
|
5227
|
+
if (pendingBytes >= 65536) flush();
|
|
5228
|
+
} else {
|
|
5229
|
+
ctx.stdout.write(text);
|
|
5230
|
+
}
|
|
5231
|
+
window = await src.take(rowSize);
|
|
5232
|
+
}
|
|
5233
|
+
if (outAbs !== null) flush();
|
|
5234
|
+
return src.failed || writeFailed ? 1 : 0;
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
|
|
4239
5238
|
function wrapStreaming(fn: CmdFn): (ctx: Ctx) => Promise<number> {
|
|
4240
5239
|
return async (ctx: Ctx) => {
|
|
4241
5240
|
try {
|
|
@@ -4243,10 +5242,10 @@ function wrapStreaming(fn: CmdFn): (ctx: Ctx) => Promise<number> {
|
|
|
4243
5242
|
const stdinObj = ctx.stdin;
|
|
4244
5243
|
const isTerminalStdin = typeof stdinObj.feed === 'function';
|
|
4245
5244
|
if (isTerminalStdin) {
|
|
4246
|
-
const
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
5245
|
+
const drainable = stdinObj as { drainBuffered?: () => string };
|
|
5246
|
+
ctx.stdin = typeof drainable.drainBuffered === 'function'
|
|
5247
|
+
? drainable.drainBuffered()
|
|
5248
|
+
: '';
|
|
4250
5249
|
}
|
|
4251
5250
|
// else: leave as pipe reader for the command to handle.
|
|
4252
5251
|
}
|
|
@@ -4289,10 +5288,10 @@ function wrap(fn: CmdFn): (ctx: Ctx) => Promise<number> {
|
|
|
4289
5288
|
// first command on a line; non-empty if the user typed
|
|
4290
5289
|
// text + Enter before the command was dispatched). DO NOT
|
|
4291
5290
|
// await — that would wait for the user's next Ctrl-D.
|
|
4292
|
-
const
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
5291
|
+
const drainable = stdinObj as { drainBuffered?: () => string };
|
|
5292
|
+
ctx.stdin = typeof drainable.drainBuffered === 'function'
|
|
5293
|
+
? drainable.drainBuffered()
|
|
5294
|
+
: '';
|
|
4296
5295
|
} else if (typeof stdinObj.readAll === 'function') {
|
|
4297
5296
|
// Pipe reader — upstream will close() after writing, so
|
|
4298
5297
|
// readAll() resolves bounded.
|
|
@@ -4364,7 +5363,12 @@ export function registerUnixCommands(
|
|
|
4364
5363
|
registry.register('readlink', wrap(withInvocationVfs(sqliteVfs, mkReadlink)));
|
|
4365
5364
|
registry.register('sha256sum', wrap(withInvocationVfs(sqliteVfs, mkSha256sum)));
|
|
4366
5365
|
registry.register('file', wrap(withInvocationVfs(sqliteVfs, mkFile)));
|
|
4367
|
-
|
|
5366
|
+
// od/hexdump/xxd read operands and sinks through ctx.vfs — the
|
|
5367
|
+
// mount-aware seam the host hands every command — so they need no
|
|
5368
|
+
// invocation-scoped raw view of their own.
|
|
5369
|
+
registry.register('xxd', wrapStreaming(mkXxd()));
|
|
5370
|
+
registry.register('od', wrapStreaming(mkOd()));
|
|
5371
|
+
registry.register('hexdump', wrapStreaming(mkHexdump()));
|
|
4368
5372
|
|
|
4369
5373
|
registry.register('chown', wrap(mkChown(sqliteVfs)));
|
|
4370
5374
|
|