feat(shell): job-aware kill, disown, jobs/set flags, fallback REPL history

- Document interactive shell vs /bin/sh in bare-os-shell man, handbook §9,
  kill(1), and shell-completion guide
- Resolve kill %n and %% to synthetic PIDs via shellBackgroundJobs
- Add disown builtin; jobs -p (pgid-only) and -l (pid column); set -o/+o to
  print errexit/nounset/pipefail/noglob
- Expand completion-engine fallback flags for common utilities
- Persist lines to /.bare/repl_history_<USER> when BARE_OS_REPL_HISTORY=1
  and Fish REPL is off (repl-session + cli-readline)
- Tests: kill job specs, set -o output, jobs -p
This commit is contained in:
Raven Scott
2026-04-21 17:26:41 -04:00
parent c50bee46eb
commit 98dd91020e
46 changed files with 15812 additions and 13956 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ Or `node packages/bare-os-coreutils/build.mjs`.
## Commands (authoritative list)
**Source of truth:** **`lib/commands.mjs`** — **`COREUTILS_COMMANDS`** (imported by **`build.mjs`** and **`scripts/build-man-db.mjs`**). Each name must have **`man/pages/<name>.json`**. **150** Tier-1 commands in the current tree (**`sshd`**s `/bin` body is emitted by **`bare-os-openssh`**); root **`pretest`** runs **`verify-man-coverage.mjs`** against this list (do not hand-maintain a duplicate comma-separated inventory here—use **`lib/commands.mjs`**, **`ls /bin`** in the guest, or **`man -k`**).
**Source of truth:** **`lib/commands.mjs`** — **`COREUTILS_COMMANDS`** (imported by **`build.mjs`** and **`scripts/build-man-db.mjs`**). Each name must have **`man/pages/<name>.json`**. **153** Tier-1 commands in the current tree (**`sshd`**s `/bin` body is emitted by **`bare-os-openssh`**); root **`pretest`** runs **`verify-man-coverage.mjs`** against this list (do not hand-maintain a duplicate comma-separated inventory here—use **`lib/commands.mjs`**, **`ls /bin`** in the guest, or **`man -k`**).
**`edit`** is a full-screen TTY buffer editor (syntax highlighting, search, save). **`nano`** is built from the same **`src/edit.js`** with the same **`lib/edit-*.js`** preamble; **`/bin/nano`** exists for familiarity, and the stock shell alias **`nano``edit`** routes **`nano`** to that utility (see **`packages/bare-os-booter/lib/shell.js`**). Both require a real TTY (**`stdout.isTTY`**).
+1
View File
@@ -21,6 +21,7 @@ const seederKernelLibBare = join(
/** Commands whose /bin script is preceded by extra library sources (no import in src). */
const preamble = {
md5sum: ['md5.js'],
sha224sum: ['sha224.js'],
sed: ['sed-engine.js'],
awk: ['awk-engine.js'],
jq: ['jq-engine.js'],
@@ -111,7 +111,9 @@ export const COREUTILS_COMMANDS = [
'seq',
'setfacl',
'sha1sum',
'sha224sum',
'sha256sum',
'sha384sum',
'sha512sum',
'sh',
'shuf',
+93
View File
@@ -0,0 +1,93 @@
/**
* SHA-224 (FIPS 180-4) — same compression as SHA-256, distinct IV, output first 224 bits.
* Bundled because many runtimes (incl. Node Web Crypto) omit SHA-224 in subtle.digest.
*/
function bareSha224DigestBytes(u8) {
const K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2
])
const W = new Uint32Array(64)
const n = u8.length
const bits = BigInt(n) * 8n
const bitLenHi = Number((bits >> 32n) & 0xffffffffn)
const bitLenLo = Number(bits & 0xffffffffn)
const padLen = (56 - ((n + 1) % 64) + 64) % 64
const total = n + 1 + padLen + 8
const buf = new Uint8Array(total)
buf.set(u8)
buf[n] = 0x80
const view = new DataView(buf.buffer)
view.setUint32(total - 8, bitLenHi, false)
view.setUint32(total - 4, bitLenLo, false)
let h0 = 0xc1059ed8
let h1 = 0x367cd507
let h2 = 0x3070dd17
let h3 = 0xf70e5939
let h4 = 0xffc00b31
let h5 = 0x68581511
let h6 = 0x64f98fa7
let h7 = 0xbefa4fa4
const rotr = (x, c) => ((x >>> c) | (x << (32 - c))) >>> 0
for (let off = 0; off < total; off += 64) {
for (let i = 0; i < 16; i++) W[i] = view.getUint32(off + i * 4, false)
for (let i = 16; i < 64; i++) {
const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ (W[i - 15] >>> 3)
const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ (W[i - 2] >>> 10)
W[i] = (W[i - 16] + s0 + W[i - 7] + s1) >>> 0
}
let a = h0
let b = h1
let c = h2
let d = h3
let e = h4
let f = h5
let g = h6
let h = h7
for (let i = 0; i < 64; i++) {
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)
const ch = (e & f) ^ (~e & g)
const t1 = (h + S1 + ch + K[i] + W[i]) >>> 0
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)
const maj = (a & b) ^ (a & c) ^ (b & c)
const t2 = (S0 + maj) >>> 0
h = g
g = f
f = e
e = (d + t1) >>> 0
d = c
c = b
b = a
a = (t1 + t2) >>> 0
}
h0 = (h0 + a) >>> 0
h1 = (h1 + b) >>> 0
h2 = (h2 + c) >>> 0
h3 = (h3 + d) >>> 0
h4 = (h4 + e) >>> 0
h5 = (h5 + f) >>> 0
h6 = (h6 + g) >>> 0
h7 = (h7 + h) >>> 0
}
const out = new Uint8Array(28)
const ov = new DataView(out.buffer)
ov.setUint32(0, h0, false)
ov.setUint32(4, h1, false)
ov.setUint32(8, h2, false)
ov.setUint32(12, h3, false)
ov.setUint32(16, h4, false)
ov.setUint32(20, h5, false)
ov.setUint32(24, h6, false)
return [...out].map((b) => b.toString(16).padStart(2, '0')).join('')
}
@@ -1,11 +1,12 @@
{
"name": "bare-os-shell",
"section": 1,
"title": "Bare OS interactive shell builtins",
"title": "Bare OS interactive shell (Issue 7inspired subset)",
"synopsis": [
"# builtins only — no full POSIX sh grammar"
"# Interactive session: builtins + /bin via ctx.execLine (see handbook §9)",
"# /bin/sh is a separate script runner — see man sh"
],
"description": "The line-at-a-time shell supports aliases, simple pipelines (simulated), redirection, and the builtins below. Compound commands (if, for, while) are not available.",
"description": "The interactive shell runs in the booter: tokenization, pipelines (simulated capture), redirection, AND-OR lists (; && ||), bounded compound commands (if/fi, while/for/case), background jobs (&), optional command substitution ($(…)) when BARE_OS_SHELL_CMDSUBST=1, globbing, and subsets of errexit (-e), nounset (-u), and pipefail. There are no forked subshells and no full POSIX sh grammar. Canonical narrative: handbook ch.9; completion/REPL: docs/reference/shell-completion-and-repl-editor.md.",
"options": [],
"aliases": [
"sh-builtins"
@@ -13,9 +14,9 @@
"keywords": [
"shell",
"builtin",
"cd",
"export",
"alias",
"pipeline",
"jobs",
"execLine",
"bare-os-shell",
"sh-builtins"
],
@@ -27,7 +28,7 @@
"alias name=value ...",
"unalias name ..."
],
"description": "Define or list command aliases. unalias removes definitions."
"description": "Define or list command aliases; unalias removes definitions."
},
{
"name": "cd",
@@ -64,13 +65,24 @@
],
"description": "Show or set shell file creation mask (stored in env UMASK)."
},
{
"name": "set",
"synopsis": [
"set -o",
"set +o",
"set -e | +e | -u | +u | -f | +f",
"set -o errexit|nounset|pipefail",
"set +o errexit|nounset|pipefail"
],
"description": "Toggle errexit (BARE_OS_SHELL_ERREXIT), nounset (BARE_OS_SHELL_NOUNSET), pipefail (BARE_OS_SHELL_PIPEFAIL), noglob (BARE_OS_SHELL_NOGLOB). Use set -o / set +o alone to print current shell options (subset)."
},
{
"name": "command",
"synopsis": [
"command -v|-V NAME",
"command ARGV..."
],
"description": "Resolve or run a command without using shell functions (none) or aliases for -v/-V."
"description": "Resolve or run a command; -v/-V skip aliases."
},
{
"name": "type",
@@ -79,6 +91,46 @@
],
"description": "Report whether NAME is a builtin or a path under PATH."
},
{
"name": "jobs",
"synopsis": [
"jobs [-l] [-p]"
],
"description": "List logical background jobs. -p prints pgid only; -l includes pgid/sid in the listing."
},
{
"name": "fg / bg / wait",
"synopsis": [
"fg [%job]",
"bg [%job]",
"wait [n | %n]",
"wait -n (with BARE_OS_SHELL_POSIX_MODE=1)"
],
"description": "Cooperative job control: fg awaits a job; bg resumes stopped jobs; wait waits for jobs by id or all."
},
{
"name": "suspend-job",
"synopsis": [
"suspend-job [%job]"
],
"description": "Mark a running background job stopped (logical); resume with fg or bg."
},
{
"name": "disown",
"synopsis": [
"disown [%job]"
],
"description": "Remove a job from the jobs table without cancelling its async work (still runs to completion)."
},
{
"name": "trap",
"synopsis": [
"trap -l",
"trap -p",
"trap CMD SIGNAL"
],
"description": "List signals, print handlers, or register synthetic trap handlers (ctx.shellTrapHandlers)."
},
{
"name": "login / logout",
"synopsis": [
@@ -100,9 +152,20 @@
"exit [n]"
],
"description": "Request booter exit with status n (builtin path)."
},
{
"name": "read",
"synopsis": [
"read [-r] [NAME ...]"
],
"description": "Optional when BARE_OS_SHELL_READ_BUILTIN=1; bounded line from shell stdin or readLine."
}
],
"seeAlso": [
{
"name": "sh",
"section": 1
},
{
"name": "help",
"section": 1
@@ -112,31 +175,27 @@
"section": 1
}
],
"bareOsNotes": "Pipelines do not use OS pipes; see handbook ch.4 and ch.9.",
"bareOsNotes": "Pipelines use simulated capture (not OS pipes). kill and wait accept %n job specs when shellBackgroundJobs is populated. Full UX (history file, tab menu, Ctrl+R) uses BARE_OS_FISH≠0 on a TTY; see shell-completion-and-repl-editor.md.",
"examples": [
{
"caption": "pipeline (simulated)",
"code": "ls -1 /bin | grep man"
},
{
"caption": "errexit + compound",
"code": "set -e\nif true; then echo ok; fi"
},
{
"caption": "background job",
"code": "sleep 1 &\njobs"
},
{
"caption": "redirect out",
"code": "echo hi > ~/hello.txt"
},
{
"caption": "append",
"code": "date >> ~/log.txt"
},
{
"caption": "alias + use",
"code": "alias ll='ls -la'\nll ~"
},
{
"caption": "export for children",
"code": "export EDITOR=ed\nman ls"
},
{
"caption": "temp var for one command",
"code": "PATH=/bin man which"
}
]
}
@@ -5,11 +5,19 @@
"synopsis": [
"kill [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of kill. Full behavior is defined in packages/bare-os-coreutils/src/kill.js.",
"description": "Send a signal to synthetic processes. Targets may be numeric PIDs, kernel/booter/shell names, negative values for logical process groups, or job specs when the interactive shell has background jobs: %n (job id n) or %% (current job), resolved to synthetic PIDs 4100+job.id for ctx.bareOsSendSignal. See handbook ch.9 and bare-os-process-table jobControlSemantics.",
"options": [],
"keywords": [
"kill",
"signal",
"job",
"bare-os",
"coreutils"
],
"seeAlso": [
{
"name": "bare-os-shell",
"section": 1
}
]
}
+1 -1
View File
@@ -5,7 +5,7 @@
"synopsis": [
"sh [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of sh. Full behavior is defined in packages/bare-os-coreutils/src/sh.js.",
"description": "Runs a shell script file line-by-line via ctx.execLine (same language as the interactive booter shell). This is not the interactive REPL; see man bare-os-shell for session builtins, pipelines, and jobs.",
"options": [],
"keywords": [
"sh",
@@ -0,0 +1,15 @@
{
"name": "sha224sum",
"section": 1,
"title": "sha224sum",
"synopsis": [
"sha224sum [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of sha224sum. Computes SHA-224 via a bundled digest (many runtimes omit SHA-224 in crypto.subtle.digest). Full behavior is defined in packages/bare-os-coreutils/src/sha224sum.js.",
"options": [],
"keywords": [
"sha224sum",
"bare-os",
"coreutils"
]
}
@@ -0,0 +1,15 @@
{
"name": "sha384sum",
"section": 1,
"title": "sha384sum",
"synopsis": [
"sha384sum [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of sha384sum. Full behavior is defined in packages/bare-os-coreutils/src/sha384sum.js.",
"options": [],
"keywords": [
"sha384sum",
"bare-os",
"coreutils"
]
}
+1 -1
View File
@@ -6,6 +6,6 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs"
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs"
}
}
@@ -933,18 +933,21 @@ function shellPage() {
return {
name: 'bare-os-shell',
section: 1,
title: 'Bare OS interactive shell builtins',
synopsis: ['# builtins only — no full POSIX sh grammar'],
title: 'Bare OS interactive shell (Issue 7inspired subset)',
synopsis: [
'# Interactive session: builtins + /bin via ctx.execLine (see handbook §9)',
'# /bin/sh is a separate script runner — see man sh'
],
description:
'The line-at-a-time shell supports aliases, simple pipelines (simulated), redirection, and the builtins below. Compound commands (if, for, while) are not available.',
'The interactive shell runs in the booter: tokenization, pipelines (simulated capture), redirection, AND-OR lists (; && ||), bounded compound commands (if/fi, while/for/case), background jobs (&), optional command substitution ($(…)) when BARE_OS_SHELL_CMDSUBST=1, globbing, and subsets of errexit (-e), nounset (-u), and pipefail. There are no forked subshells and no full POSIX sh grammar. Canonical narrative: handbook ch.9; completion/REPL: docs/reference/shell-completion-and-repl-editor.md.',
options: [],
aliases: ['sh-builtins'],
keywords: [
'shell',
'builtin',
'cd',
'export',
'alias',
'pipeline',
'jobs',
'execLine',
'bare-os-shell',
'sh-builtins'
],
@@ -953,7 +956,7 @@ function shellPage() {
name: 'alias',
synopsis: ['alias', 'alias name=value ...', 'unalias name ...'],
description:
'Define or list command aliases. unalias removes definitions.'
'Define or list command aliases; unalias removes definitions.'
},
{
name: 'cd',
@@ -982,17 +985,63 @@ function shellPage() {
description:
'Show or set shell file creation mask (stored in env UMASK).'
},
{
name: 'set',
synopsis: [
'set -o',
'set +o',
'set -e | +e | -u | +u | -f | +f',
'set -o errexit|nounset|pipefail',
'set +o errexit|nounset|pipefail'
],
description:
'Toggle errexit (BARE_OS_SHELL_ERREXIT), nounset (BARE_OS_SHELL_NOUNSET), pipefail (BARE_OS_SHELL_PIPEFAIL), noglob (BARE_OS_SHELL_NOGLOB). Use set -o / set +o alone to print current shell options (subset).'
},
{
name: 'command',
synopsis: ['command -v|-V NAME', 'command ARGV...'],
description:
'Resolve or run a command without using shell functions (none) or aliases for -v/-V.'
description: 'Resolve or run a command; -v/-V skip aliases.'
},
{
name: 'type',
synopsis: ['type NAME'],
description: 'Report whether NAME is a builtin or a path under PATH.'
},
{
name: 'jobs',
synopsis: ['jobs [-l] [-p]'],
description:
'List logical background jobs. -p prints pgid only; -l includes pgid/sid in the listing.'
},
{
name: 'fg / bg / wait',
synopsis: [
'fg [%job]',
'bg [%job]',
'wait [n | %n]',
'wait -n (with BARE_OS_SHELL_POSIX_MODE=1)'
],
description:
'Cooperative job control: fg awaits a job; bg resumes stopped jobs; wait waits for jobs by id or all.'
},
{
name: 'suspend-job',
synopsis: ['suspend-job [%job]'],
description:
'Mark a running background job stopped (logical); resume with fg or bg.'
},
{
name: 'disown',
synopsis: ['disown [%job]'],
description:
'Remove a job from the jobs table without cancelling its async work (still runs to completion).'
},
{
name: 'trap',
synopsis: ['trap -l', 'trap -p', 'trap CMD SIGNAL'],
description:
'List signals, print handlers, or register synthetic trap handlers (ctx.shellTrapHandlers).'
},
{
name: 'login / logout',
synopsis: ['login [--new] passphrase...', 'logout [--save]'],
@@ -1008,20 +1057,30 @@ function shellPage() {
name: 'exit',
synopsis: ['exit [n]'],
description: 'Request booter exit with status n (builtin path).'
},
{
name: 'read',
synopsis: ['read [-r] [NAME ...]'],
description:
'Optional when BARE_OS_SHELL_READ_BUILTIN=1; bounded line from shell stdin or readLine.'
}
],
seeAlso: [
{ name: 'sh', section: 1 },
{ name: 'help', section: 1 },
{ name: 'man', section: 1 }
],
bareOsNotes: 'Pipelines do not use OS pipes; see handbook ch.4 and ch.9.',
bareOsNotes:
'Pipelines use simulated capture (not OS pipes). kill and wait accept %n job specs when shellBackgroundJobs is populated. Full UX (history file, tab menu, Ctrl+R) uses BARE_OS_FISH≠0 on a TTY; see shell-completion-and-repl-editor.md.',
examples: [
{ caption: 'pipeline (simulated)', code: 'ls -1 /bin | grep man' },
{
caption: 'errexit + compound',
code: 'set -e\nif true; then echo ok; fi'
},
{ caption: 'background job', code: 'sleep 1 &\njobs' },
{ caption: 'redirect out', code: 'echo hi > ~/hello.txt' },
{ caption: 'append', code: 'date >> ~/log.txt' },
{ caption: 'alias + use', code: "alias ll='ls -la'\nll ~" },
{ caption: 'export for children', code: 'export EDITOR=ed\nman ls' },
{ caption: 'temp var for one command', code: 'PATH=/bin man which' }
{ caption: 'alias + use', code: "alias ll='ls -la'\nll ~" }
]
}
}
+36 -2
View File
@@ -1,3 +1,34 @@
/**
* Resolve job spec %n / %% to synthetic PID (4100+job.id) for ctx.bareOsSendSignal.
* Matches fg/wait job selection: %% is latest non-done job.
* @param {string} raw
* @param {Record<string, unknown>} ctx
* @returns {string} numeric pid, pgid negative form, or passthrough token
*/
function bareOsKillResolveTarget(raw, ctx) {
const s = String(raw == null ? '' : raw).trim()
if (!s.startsWith('%')) return s
const list =
ctx.shellBackgroundJobs &&
typeof ctx.shellBackgroundJobs === 'object' &&
Array.isArray(ctx.shellBackgroundJobs.list)
? ctx.shellBackgroundJobs.list
: []
const spec = s.slice(1)
if (spec === '%' || spec === '') {
const running = list.filter((j) => j && !j.done)
const j = running.length ? running[running.length - 1] : null
if (!j) throw new Error('kill: no current job')
return String(4100 + j.id)
}
const id = Number.parseInt(spec, 10)
if (!Number.isFinite(id))
throw new Error('kill: invalid job specification: ' + raw)
const j = list.find((x) => x && x.id === id)
if (!j) throw new Error('kill: %' + id + ': no such job')
return String(4100 + j.id)
}
async function run(ctx, argv) {
const args = argv.slice(1)
let signal = 'TERM'
@@ -33,7 +64,9 @@ async function run(ctx, argv) {
}
if (targets.length === 0) {
ctx.console.error('usage: kill [-s SIGNAL | -SIGNAL] <pid|name>...')
ctx.console.error(
'usage: kill [-s SIGNAL | -SIGNAL] <pid|name|%job|%%>...'
)
ctx.exitCode = 1
return
}
@@ -47,7 +80,8 @@ async function run(ctx, argv) {
let failed = 0
for (const t of targets) {
try {
send.call(ctx, t, signal)
const resolved = bareOsKillResolveTarget(t, ctx)
send.call(ctx, resolved, signal)
} catch (e) {
failed++
ctx.console.error('kill: ' + t + ': ' + (e?.message || String(e)))
@@ -0,0 +1,52 @@
/**
* sha224sum — compute SHA-224 checksums (hex), GNU-like output line.
* Uses bundled SHA-224 (many runtimes omit SHA-224 in crypto.subtle.digest).
*/
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: sha224sum [FILE]...\n' +
'With no FILE, or when FILE is -, read standard input.'
)
ctx.exitCode = 0
return
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('sha224sum: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
function one(name, buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
try {
const hex = bareSha224DigestBytes(u8)
ctx.console.log(hex + ' ' + name)
} catch (e) {
ctx.console.error('sha224sum: ' + (e.message || e))
ctx.exitCode = 1
}
}
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
one('-', b4.from(bareStdin(ctx)))
return
}
for (const p of paths) {
if (p === '-') {
one('-', b4.from(bareStdin(ctx)))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('sha224sum: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
one(p, b)
}
}
@@ -0,0 +1,59 @@
async function sha384Hex(u8) {
const subtle = globalThis.crypto?.subtle
if (!subtle || typeof subtle.digest !== 'function') {
throw new Error('crypto.subtle.digest (SHA-384) is not available')
}
const hash = await subtle.digest('SHA-384', u8)
return [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: sha384sum [FILE]...\n' +
'With no FILE, or when FILE is -, read standard input.'
)
ctx.exitCode = 0
return
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('sha384sum: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
async function one(name, buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
try {
const hex = await sha384Hex(u8)
ctx.console.log(hex + ' ' + name)
} catch (e) {
ctx.console.error('sha384sum: ' + (e.message || e))
ctx.exitCode = 1
}
}
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
await one('-', b4.from(bareStdin(ctx)))
return
}
for (const p of paths) {
if (p === '-') {
await one('-', b4.from(bareStdin(ctx)))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('sha384sum: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
await one(p, b)
}
}
@@ -0,0 +1,54 @@
/**
* SHA-224 bundled digest vectors; SHA-384 via Web Crypto (same as sha384sum command).
*/
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import vm from 'node:vm'
import test from 'brittle'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const sha224Lib = path.join(__dirname, '../lib/sha224.js')
function loadBareSha224() {
const src = readFileSync(sha224Lib, 'utf8')
const ctx = {}
vm.createContext(ctx)
vm.runInContext(src + '\nthis.__fn = bareSha224DigestBytes;', ctx)
return ctx.__fn
}
test('bareSha224DigestBytes matches known vectors', async (t) => {
const bareSha224DigestBytes = loadBareSha224()
t.is(
bareSha224DigestBytes(new Uint8Array(0)),
'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f'
)
t.is(
bareSha224DigestBytes(new TextEncoder().encode('abc')),
'23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7'
)
})
test('SHA-384 empty and abc via subtle.digest', async (t) => {
const subtle = globalThis.crypto?.subtle
if (!subtle?.digest) {
t.pass('skip: no Web Crypto subtle')
return
}
async function hex384(u8) {
const hash = await subtle.digest('SHA-384', u8)
return [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
t.is(
await hex384(new Uint8Array(0)),
'38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b'
)
const abc = new TextEncoder().encode('abc')
t.is(
await hex384(abc),
'cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7'
)
})