/** * Hardening regressions for high-traffic /bin utilities. */ import { readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import test from 'brittle' import b4a from 'b4a' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor async function loadBin(name) { const runtime = await readFile(path.join(__dirname, '../lib/runtime.js'), 'utf8') const bodyRaw = await readFile(path.join(__dirname, `../src/${name}.js`), 'utf8') const body = bodyRaw.replace(/^export\s+\{[^}]*\}\s*;?\s*$/gm, '') return new AsyncFunction( 'ctx', 'argv', `${runtime}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n` ) } function mkCtx(vfs = {}) { const logs = [] const errs = [] const raw = [] return { b4a, exitCode: 0, shellStdin: '', console: { log: (m) => logs.push(String(m)), error: (m) => errs.push(String(m)) }, vfs: { env: {}, ...vfs }, bareOsBinWrite: (u8) => { raw.push(u8 instanceof Uint8Array ? u8 : b4a.from(u8)) }, _logs: logs, _errs: errs, _raw: raw } } function memVfs(files = {}) { const store = { ...files } return { env: {}, files: store, resolveLogical: (p) => String(p || ''), async lstat(p) { if (!Object.prototype.hasOwnProperty.call(store, p)) return null const e = store[p] if (e && e.stat) return e.stat return { type: 'file', mode: 0o644, mtimeMs: 1, size: e ? e.length : 0 } }, async stat(p) { return this.lstat(p) }, async readFile(p) { if (!Object.prototype.hasOwnProperty.call(store, p)) return null const e = store[p] if (e && e.body) return e.body return e }, async writeFile(p, buf, opts) { store[p] = buf return opts }, async unlink(p) { if (!Object.prototype.hasOwnProperty.call(store, p)) { const err = new Error('ENOENT') err.code = 'ENOENT' throw err } delete store[p] }, async mkdir(p) { store[p] = { stat: { type: 'directory', mode: 0o755, mtimeMs: 1 } } }, async readdir(p) { const prefix = String(p).replace(/\/+$/, '') + '/' const names = new Set() for (const k of Object.keys(store)) { if (k.startsWith(prefix)) { const rest = k.slice(prefix.length) if (rest && !rest.includes('/')) names.add(rest) } } return [...names] }, async rm(p) { if (!Object.prototype.hasOwnProperty.call(store, p)) { const err = new Error('ENOENT') err.code = 'ENOENT' throw err } delete store[p] }, async chmod() { return true }, async chown() { return true } } } test('mv refuses self-move', async (t) => { const run = await loadBin('mv') const vfs = memVfs({ '/a': b4a.from('keep') }) const ctx = mkCtx(vfs) await run(ctx, ['mv', '/a', '/a']) t.is(ctx.exitCode, 1) t.ok(vfs.files['/a']) t.ok(ctx._errs.join('\n').includes('cannot move')) }) test('cp refuses copy into self', async (t) => { const run = await loadBin('cp') const vfs = memVfs({ '/dir': { stat: { type: 'directory', mode: 0o755 } }, '/dir/f': b4a.from('x') }) const ctx = mkCtx(vfs) await run(ctx, ['cp', '-R', '/dir', '/dir']) t.is(ctx.exitCode, 1) t.ok(ctx._errs.join('\n').includes('itself')) }) test('touch does not replace a directory with an empty file', async (t) => { const run = await loadBin('touch') const vfs = memVfs({ '/d': { stat: { type: 'directory', mode: 0o755, mtimeMs: 1 } } }) const ctx = mkCtx(vfs) await run(ctx, ['touch', '/d']) t.is(ctx.exitCode, 0) t.is(vfs.files['/d'].stat.type, 'directory') }) test('truncate does not clobber unread existing file', async (t) => { const run = await loadBin('truncate') const vfs = memVfs({ '/secret': { stat: { type: 'file', mode: 0o600, mtimeMs: 1 } } }) vfs.readFile = async () => null const ctx = mkCtx(vfs) await run(ctx, ['truncate', '-s', '0', '/secret']) t.is(ctx.exitCode, 1) t.ok(ctx._errs.join('\n').includes('cannot read')) }) test('rm -i is rejected rather than deleting', async (t) => { const run = await loadBin('rm') const vfs = memVfs({ '/f': b4a.from('x') }) const ctx = mkCtx(vfs) await run(ctx, ['rm', '-i', '/f']) t.is(ctx.exitCode, 1) t.ok(vfs.files['/f']) t.ok(ctx._errs.join('\n').includes('invalid option')) }) test('rm -f with no operands exits 0', async (t) => { const run = await loadBin('rm') const ctx = mkCtx(memVfs()) await run(ctx, ['rm', '-f']) t.is(ctx.exitCode, 0) }) test('head -n 0 prints nothing; missing file exits 1', async (t) => { const run = await loadBin('head') const ctx = mkCtx({ env: {}, async readFile() { return b4a.from('a\nb\n') } }) ctx.shellStdin = 'a\nb\n' await run(ctx, ['head', '-n', '0']) t.is(ctx.exitCode, 0) t.is(ctx._logs.join(''), '') const ctx2 = mkCtx({ env: {}, async readFile() { return null } }) await run(ctx2, ['head', '/missing']) t.is(ctx2.exitCode, 1) }) test('wc missing file exits 1', async (t) => { const run = await loadBin('wc') const ctx = mkCtx({ env: {}, async readFile() { return null } }) await run(ctx, ['wc', '/missing']) t.is(ctx.exitCode, 1) }) test('du missing path exits 1', async (t) => { const run = await loadBin('du') const ctx = mkCtx({ env: {}, resolveLogical: (p) => p, async lstat() { return null }, async stat() { return null } }) await run(ctx, ['du', '/nope']) t.is(ctx.exitCode, 1) t.ok(ctx._errs.join('\n').includes('No such file')) }) test('mkdir without -p fails if directory exists', async (t) => { const run = await loadBin('mkdir') const ctx = mkCtx({ env: {}, async lstat() { return { type: 'directory' } }, async mkdir() { throw new Error('should not mkdir') } }) await run(ctx, ['mkdir', '/already']) t.is(ctx.exitCode, 1) t.ok(ctx._errs.join('\n').includes('File exists')) }) test('base64 decode uses portable alphabet (not Node Buffer)', async (t) => { const run = await loadBin('base64') const ctx = mkCtx() ctx.b4a = { from: (s) => new TextEncoder().encode(String(s)), toString: (u8) => new TextDecoder().decode(u8) } ctx.shellStdin = 'YWJj' await run(ctx, ['base64', '-d']) t.is(ctx.exitCode, 0) t.is(new TextDecoder().decode(ctx._raw[0]), 'abc') }) test('basenc hex decode rejects junk', async (t) => { const run = await loadBin('basenc') const ctx = mkCtx() ctx.shellStdin = 'zzzz' await run(ctx, ['basenc', '--base16', '-d']) t.is(ctx.exitCode, 1) t.ok(ctx._errs.join('\n').includes('invalid hex')) }) test('seq -5 5 works and huge spans are capped', async (t) => { const run = await loadBin('seq') const ctx = mkCtx() await run(ctx, ['seq', '-5', '5']) t.is(ctx.exitCode, 0) t.ok(ctx._logs.includes('-5')) t.ok(ctx._logs.includes('5')) const ctx2 = mkCtx({ env: { BARE_OS_SEQ_MAX_LINES: '10' } }) await run(ctx2, ['seq', '1', '100']) t.is(ctx2.exitCode, 1) t.ok(ctx2._errs.join('\n').includes('exceeds')) }) test('grep -m 0 matches nothing', async (t) => { const run = await loadBin('grep') const ctx = mkCtx() ctx.shellStdin = 'foo\nbar\n' await run(ctx, ['grep', '-m', '0', 'foo']) t.is(ctx.exitCode, 1) t.is(ctx._logs.join(''), '') }) test('expr zero result exits 1', async (t) => { const run = await loadBin('expr') const ctx = mkCtx() await run(ctx, ['expr', '1', '-', '1']) t.is(ctx.exitCode, 1) t.is(ctx._logs.join(''), '0') }) test('cmp missing file exits 2', async (t) => { const run = await loadBin('cmp') const ctx = mkCtx({ async readFile() { return null } }) await run(ctx, ['cmp', '/a', '/b']) t.is(ctx.exitCode, 2) }) test('pathchk empty path exits 1', async (t) => { const run = await loadBin('pathchk') const ctx = mkCtx() await run(ctx, ['pathchk', '']) t.is(ctx.exitCode, 1) }) test('chmod rejects garbage symbolic mode', async (t) => { const run = await loadBin('chmod') const ctx = mkCtx({ async stat() { return { type: 'file', mode: 0o644 } }, async chmod() { return true } }) await run(ctx, ['chmod', 'garbage', '/f']) t.is(ctx.exitCode, 1) t.ok(ctx._errs.join('\n').includes('invalid mode')) }) test('sum empty file reports 0 blocks', async (t) => { const run = await loadBin('sum') const ctx = mkCtx() ctx.shellStdin = '' await run(ctx, ['sum']) t.is(ctx.exitCode, 0) t.ok(ctx._logs[0].startsWith('0\t0\t')) }) test('hostname set does not throw', async (t) => { const run = await loadBin('hostname') const ctx = mkCtx() await run(ctx, ['hostname', '-S', 'box']) t.is(ctx.exitCode, 1) t.ok(ctx._errs.join('\n').includes('setting disabled')) }) test('sleep missing operand exits 1', async (t) => { const run = await loadBin('sleep') const ctx = mkCtx() await run(ctx, ['sleep']) t.is(ctx.exitCode, 1) }) test('factor rejects non-integers', async (t) => { const run = await loadBin('factor') const ctx = mkCtx() await run(ctx, ['factor', 'foo']) t.is(ctx.exitCode, 1) }) test('tsort odd token count exits 1', async (t) => { const run = await loadBin('tsort') const ctx = mkCtx() ctx.shellStdin = 'a b c' await run(ctx, ['tsort']) t.is(ctx.exitCode, 1) }) test('unlink -- -foo unlinks dashed name', async (t) => { const run = await loadBin('unlink') const seen = [] const ctx = mkCtx({ async unlink(p) { seen.push(p) } }) await run(ctx, ['unlink', '--', '-foo']) t.is(ctx.exitCode, 0) t.alike(seen, ['-foo']) })