@nimbus-sh/core 0.6.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.
Files changed (88) hide show
  1. package/README.md +47 -26
  2. package/dist/_shared/byte-stream.d.ts +5 -4
  3. package/dist/_shared/byte-stream.d.ts.map +1 -1
  4. package/dist/_shared/real-node-imports.d.ts +7 -0
  5. package/dist/_shared/real-node-imports.d.ts.map +1 -1
  6. package/dist/_shared/real-node-imports.js +8 -0
  7. package/dist/_shared/tarball.d.ts +23 -5
  8. package/dist/_shared/tarball.d.ts.map +1 -1
  9. package/dist/_shared/tarball.js +38 -5
  10. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
  11. package/dist/_shared/vfs-write-ledger.js +22 -16
  12. package/dist/runtime/comment-strip.d.ts +0 -35
  13. package/dist/runtime/comment-strip.d.ts.map +1 -1
  14. package/dist/runtime/comment-strip.js +48 -20
  15. package/dist/runtime/esbuild-service.d.ts +38 -42
  16. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  17. package/dist/runtime/esbuild-service.js +494 -197
  18. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  19. package/dist/runtime/javascript-ast.js +45 -7
  20. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  21. package/dist/runtime/opentui-wasm-backend.js +18 -16
  22. package/dist/runtime/port-registry.d.ts +4 -2
  23. package/dist/runtime/port-registry.d.ts.map +1 -1
  24. package/dist/runtime/port-registry.js +60 -8
  25. package/dist/shell/stdin-adapter.d.ts +11 -0
  26. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  27. package/dist/shell/stdin-adapter.js +59 -0
  28. package/dist/shell/unix-commands.d.ts +2 -1
  29. package/dist/shell/unix-commands.d.ts.map +1 -1
  30. package/dist/shell/unix-commands.js +1078 -50
  31. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  32. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  33. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  34. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  35. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  36. package/dist/substrate/lifo/commands/system/npm.js +26 -43
  37. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  38. package/dist/substrate/lifo/commands/text/head.js +2 -1
  39. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  40. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  41. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  42. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  43. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  44. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  45. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  46. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  47. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  48. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  49. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  50. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  51. package/dist/substrate/lifo/shell/pipe.js +62 -18
  52. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  53. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  54. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  55. package/dist/vfs/sqlite-vfs.d.ts +19 -0
  56. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  57. package/dist/vfs/sqlite-vfs.js +106 -23
  58. package/dist/workspace/nimbus-workspace.d.ts +15 -0
  59. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  60. package/dist/workspace/nimbus-workspace.js +20 -3
  61. package/dist/workspace/supervisor-op.d.ts +23 -0
  62. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  63. package/dist/workspace/supervisor-op.js +92 -0
  64. package/package.json +2 -2
  65. package/src/_shared/byte-stream.ts +5 -4
  66. package/src/_shared/real-node-imports.ts +8 -0
  67. package/src/_shared/tarball.ts +56 -5
  68. package/src/_shared/vfs-write-ledger.ts +22 -16
  69. package/src/runtime/comment-strip.ts +42 -23
  70. package/src/runtime/esbuild-service.ts +566 -217
  71. package/src/runtime/javascript-ast.ts +40 -9
  72. package/src/runtime/opentui-wasm-backend.ts +19 -17
  73. package/src/runtime/port-registry.ts +66 -8
  74. package/src/shell/stdin-adapter.ts +58 -0
  75. package/src/shell/unix-commands.ts +1054 -50
  76. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  77. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  78. package/src/substrate/lifo/commands/system/npm.ts +58 -38
  79. package/src/substrate/lifo/commands/text/head.ts +2 -1
  80. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  81. package/src/substrate/lifo/commands/types.ts +8 -5
  82. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  83. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  84. package/src/substrate/lifo/shell/pipe.ts +65 -24
  85. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  86. package/src/vfs/sqlite-vfs.ts +104 -23
  87. package/src/workspace/nimbus-workspace.ts +26 -2
  88. package/src/workspace/supervisor-op.ts +117 -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, realpath
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
- const chunk = reader.readBytes ? await reader.readBytes(want) : await reader.read();
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 = enc.encode(chunk).subarray(0, want);
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
- function mkXxd(vfs) {
4462
- return (ctx) => {
4463
- const file = ctx.args.find(a => !a.startsWith('-'));
4464
- if (!file) {
4465
- ctx.stderr.write('Usage: xxd FILE\n');
4466
- return 1;
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
- const fp = resolvePath(ctx.cwd, file);
4469
- try {
4470
- const data = vfs.readFile(fp);
4471
- const len = Math.min(data.length, ctx.args.includes('-l') ? parseInt(ctx.args[ctx.args.indexOf('-l') + 1]) || 256 : 256);
4472
- for (let i = 0; i < len; i += 16) {
4473
- const hex = Array.from(data.slice(i, i + 16)).map(b => b.toString(16).padStart(2, '0')).join(' ');
4474
- const ascii = Array.from(data.slice(i, i + 16)).map(b => b >= 32 && b < 127 ? String.fromCharCode(b) : '.').join('');
4475
- ctx.stdout.write(`${i.toString(16).padStart(8, '0')}: ${hex.padEnd(48)} ${ascii}\n`);
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
- return 0;
4635
+ radix = value;
4478
4636
  }
4479
- catch {
4480
- ctx.stderr.write(`xxd: ${file}: No such file\n`);
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
- // ── Registration ────────────────────────────────────────────────────────
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
- * Wrap a sync/async command so it always returns Promise<number>.
4488
- * The LIFO shell calls .then() on the return value of every command,
4489
- * so raw numbers cause "E3(...).then is not a function".
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
- * SHELL-R6-B2 follow-on: wrapStreaming for commands that handle pipe
4497
- * readers directly (head, tail, etc commands that can terminate
4498
- * before the producer drains).
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
- * Behavior:
4501
- * - If ctx.stdin is a terminal stdin (has .feed), drain buffered
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 buf = Array.isArray(stdinObj.buffer)
4519
- ? stdinObj.buffer.splice(0)
4520
- : [];
4521
- ctx.stdin = buf.join('');
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 buf = Array.isArray(stdinObj.buffer)
4565
- ? stdinObj.buffer.splice(0)
4566
- : [];
4567
- ctx.stdin = buf.join('');
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
- registry.register('xxd', wrap(withInvocationVfs(sqliteVfs, mkXxd)));
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
  /**