Complete the internal “100 task” roadmap: coreutils and shell parity (xargs,

sh, diff/patch, sort, printf, find, test, getfacl/setfacl/xattr), expanded
/proc and metrics (process table, syscalls, replication, net, security
posture, worker budget, swarm/replication hints), initd DAG supervision
metadata and richer restart journal telemetry, synthetic process groups via
IPC (assignProcessGroup/signalProcessGroup) mirrored into process_table,
optional kernel.ext.d incremental hot reload (BARE_OS_KERNEL_EXT_D_HOT_RELOAD)
with reload audit NDJSON, features proc for hyperblobs dedup and systemd
subset documentation, vault threat model doc plus posture fields for AEAD,
Pear enclave pointer, account rotation continuity, and Ed25519 consistency
across boot manifest / extensions / replication. Adds or extends tests and
keeps kernel/ and packages/bare-os-seeder/kernel/ in parity; guest init is
bundled from kernel/lib/init/init-main.js via bundle-kernel-init.
This commit is contained in:
Raven Scott
2026-04-04 21:23:49 -04:00
parent c29ec13cf9
commit 346bb71ffe
177 changed files with 8087 additions and 876 deletions
@@ -26,6 +26,7 @@ var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [
['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'],
['swarm', '/proc/bare_os/swarm'],
['syncWindow', '/proc/bare_os/sync_window.json'],
['clock', '/proc/bare_os/clock.json'],
['hdmsHealth', '/proc/bare_os/hdms_health.json'],
['hdmsHints', '/proc/bare_os/hdms_hints.json'],
['dhtStatus', '/proc/bare_os/dht_status.json'],
@@ -26,6 +26,7 @@ export const COREUTILS_COMMANDS = [
'date',
'dd',
'df',
'diff',
'dir',
'dirname',
'dircolors',
@@ -42,6 +43,7 @@ export const COREUTILS_COMMANDS = [
'fmt',
'fold',
'getconf',
'getfacl',
'git-pear',
'grep',
'groups',
@@ -86,6 +88,7 @@ export const COREUTILS_COMMANDS = [
'openssl',
'oidc-publish',
'paste',
'patch',
'pathchk',
'pr',
'printenv',
@@ -101,9 +104,11 @@ export const COREUTILS_COMMANDS = [
'savevault',
'sed',
'seq',
'setfacl',
'sha1sum',
'sha256sum',
'sha512sum',
'sh',
'shuf',
'sleep',
'sort',
@@ -140,6 +145,7 @@ export const COREUTILS_COMMANDS = [
'which',
'who',
'whoami',
'xattr',
'xargs',
'yes'
]
@@ -5,8 +5,22 @@
"synopsis": [
"crontab [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of user crontab manipulation. Full behavior is defined in packages/bare-os-coreutils/src/crontab.js.",
"options": [],
"description": "Bare OS implementation of user crontab manipulation. Full behavior is defined in packages/bare-os-coreutils/src/crontab.js. Timer backends honor BARE_OS_TIMER_EVERY_MS_MONOTONIC for monotonic chained schedules in bare-cron.",
"options": [
{
"flag": "-l",
"meaning": "List the current crontab on stdout"
},
{
"flag": "-r",
"meaning": "Remove all jobs for the user"
},
{
"flag": "-e",
"meaning": "Edit via ctx (stub when no editor bridge)"
}
],
"bareOsNotes": "See packages/bare-os-booter/lib/bare-cron.js and BARE_OS_TIMER_EVERY_MS_MONOTONIC in env reference for POSIX-adjacent timer behavior.",
"keywords": [
"crontab",
"bare-os",
@@ -0,0 +1,17 @@
{
"name": "diff",
"section": 1,
"title": "compare two files",
"synopsis": ["diff [-q] [-s] [-u] FILE1 FILE2"],
"description": "Compares two text files on the VFS. **`-q`** prints nothing but sets exit status. **`-s`** reports when files are identical. **`-u`** emits a single unified hunk suitable for **`patch(1)`** (Bare OS subset).",
"options": [
{ "flag": "-q", "meaning": "Quiet; exit status only" },
{ "flag": "-s", "meaning": "Report when files are the same" },
{ "flag": "-u", "meaning": "Unified diff (one hunk)" }
],
"keywords": ["diff", "compare", "bare-os", "coreutils"],
"bareOsNotes": "Not full POSIX diff; binary files treated as UTF-8 text. See **`/etc/bare-os/posix_utilities.json`**.",
"examples": [
{ "caption": "unified diff for patching", "code": "diff -u a.txt b.txt" }
]
}
@@ -0,0 +1,21 @@
{
"name": "getfacl",
"section": 1,
"title": "display synthetic access control lists",
"synopsis": ["getfacl [-c] FILE"],
"description": "Prints ACL text from FILE.bare_acl when present; otherwise synthesizes user::, group::, and other:: triples from the file mode bits.",
"options": [
{
"flag": "-c",
"meaning": "Omit comment header lines"
}
],
"keywords": ["getfacl", "acl", "bare-os", "coreutils"],
"bareOsNotes": "Not Linux NFS ACLs; sidecar contract matches getconf BARE_OS_ACL_SIDECAR_SUFFIX.",
"examples": [
{
"caption": "show ACL or synthesized mode triples",
"code": "getfacl /tmp/a"
}
]
}
@@ -0,0 +1,16 @@
{
"name": "patch",
"section": 1,
"title": "apply a diff file",
"synopsis": ["patch [-pNUM] [--dry-run] [FILE]"],
"description": "Reads a unified diff from **`stdin`** (`ctx.shellStdin`) and applies it to **`FILE`** or the path from the **`+++`** header. Supports **`-p`** path strip count and **`--dry-run`** (verify hunk without writing).",
"options": [
{ "flag": "-p NUM", "meaning": "Strip NUM leading path segments from paths in the patch" },
{ "flag": "--dry-run, --check", "meaning": "Check patch without modifying files" }
],
"keywords": ["patch", "diff", "bare-os", "coreutils"],
"bareOsNotes": "Minimal hunk parser; only patches matching the whole-file content implied by the hunk. See **`src/patch.js`**.",
"examples": [
{ "caption": "apply from shell stdin redirection", "code": "patch -p0 < fix.patch" }
]
}
@@ -0,0 +1,21 @@
{
"name": "setfacl",
"section": 1,
"title": "set synthetic access control lists",
"synopsis": ["setfacl [-b] PATH"],
"description": "Writes ACL lines from stdin to PATH.bare_acl, or removes the sidecar with -b.",
"options": [
{
"flag": "-b",
"meaning": "Remove ACL sidecar if it exists"
}
],
"keywords": ["setfacl", "acl", "bare-os", "coreutils"],
"bareOsNotes": "Requires non-empty ctx.shellStdin when not using -b (same pattern as patch).",
"examples": [
{
"caption": "install ACL text from a here-doc in the shell",
"code": "printf 'user::rw-\\n' | setfacl /tmp/a"
}
]
}
@@ -0,0 +1,20 @@
{
"name": "sh",
"section": 1,
"title": "run a shell script (minimal)",
"synopsis": ["sh SCRIPT"],
"description": "Reads SCRIPT from the VFS, strips one leading `#!` line, then runs each non-empty, non-comment line via **`ctx.execLine`** (same pipeline and builtins as the interactive shell). Not a full POSIX `sh`; no `-c`, here-documents, or functions.",
"options": [],
"keywords": ["sh", "shebang", "bare-os", "coreutils"],
"bareOsNotes": "Requires **`ctx.execLine`**. POSIX coverage index: **`/etc/bare-os/posix_utilities.json`** (utilities.sh). Symlink targets are followed when opening SCRIPT.",
"examples": [
{
"caption": "run a small script from $PWD",
"code": "sh ./setup.sh"
},
{
"caption": "shebang line is ignored for execution",
"code": "#!/bin/sh\necho hello"
}
]
}
@@ -5,7 +5,7 @@
"synopsis": [
"tar [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of tar. Full behavior is defined in packages/bare-os-coreutils/src/tar.js.",
"description": "Bare OS implementation of tar. Full behavior is defined in packages/bare-os-coreutils/src/tar.js. Extended attributes for unpacked files may be mirrored manually using sibling PATH.bare_xattr.json sidecars (see xattr(1)).",
"options": [],
"keywords": [
"tar",
@@ -5,7 +5,7 @@
"synopsis": [
"xargs [OPTION]... [OPERAND]..."
],
"description": "Reads stdin into argument batches and runs **`ctx.runBinCommand`** (same as the shell). Enforces stdin size, token count, batch size, and invocation limits for safety.",
"description": "Reads stdin into argument batches and runs **`ctx.runBinCommand`** (same as the shell). Enforces stdin size, token count, batch size, and invocation limits for safety. Parallel **`-P`** runs disjoint batches concurrently using shallow ctx clones so **`exitCode`** does not race.",
"options": [
{
"flag": "-0, --null",
@@ -14,6 +14,14 @@
{
"flag": "-n, --max-args",
"meaning": "Up to N arguments per utility invocation (capped at 128)"
},
{
"flag": "-I repl",
"meaning": "Replace repl in the utility argv; implies **`-n 1`** unless **`-n`** was set"
},
{
"flag": "-P, --max-procs",
"meaning": "Run up to N batches in parallel (capped by **`BARE_OS_XARGS_MAX_PROCS`**, max 32; default 8)"
}
],
"keywords": [
@@ -21,7 +29,7 @@
"bare-os",
"coreutils"
],
"bareOsNotes": "No host process spawn; not full POSIX xargs (no -I, -P, etc.). See src/xargs.js for limits.",
"bareOsNotes": "No host process spawn; not full POSIX xargs. See **`src/xargs.js`** for limits. POSIX coverage: **`/etc/bare-os/posix_utilities.json`** (utilities.xargs).",
"examples": [
{
"caption": "pass lines as arguments",
@@ -31,6 +39,10 @@
"caption": "one argument per run",
"code": "printf 'a\\nb\\n' | xargs -n1 echo"
},
{
"caption": "two parallel batches (when safe for the utility)",
"code": "printf 'a\\nb\\n' | xargs -P2 -n1 echo"
},
{
"caption": "workaround for complex scripts",
"code": "# for f in *.txt; do grep -l foo $f; done"
@@ -0,0 +1,29 @@
{
"name": "xattr",
"section": 1,
"title": "extended attributes via JSON sidecar",
"synopsis": ["xattr [-l] [-w NAME VALUE] [-d NAME] PATH"],
"description": "Manipulates PATH.bare_xattr.json, a JSON object of extended attribute names to base64-encoded UTF-8 values.",
"options": [
{
"flag": "-l",
"meaning": "List names and decoded values"
},
{
"flag": "-w NAME VALUE",
"meaning": "Set attribute"
},
{
"flag": "-d NAME",
"meaning": "Delete attribute"
}
],
"keywords": ["xattr", "bare-os", "coreutils"],
"bareOsNotes": "Sidecar suffix matches getconf BARE_OS_XATTR_SIDECAR_SUFFIX.",
"examples": [
{
"caption": "set and list",
"code": "xattr -w user.tag hello /tmp/a && xattr -l /tmp/a"
}
]
}
+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/posix-test-int-compare.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/posix-test-int-compare.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs"
}
}
+5 -4
View File
@@ -1,8 +1,9 @@
async function run(ctx, argv) {
const silent = argv.includes('-s') || argv.includes('--silent')
const args = argv.filter((a) => !a.startsWith('-'))
const a = args[0]
const b = args[1]
const rest = argv.slice(1)
const silent = rest.includes('-s') || rest.includes('--silent')
const paths = rest.filter((a) => !a.startsWith('-'))
const a = paths[0]
const b = paths[1]
if (!a || !b) {
ctx.console.error('usage: cmp [-s] file1 file2')
ctx.exitCode = 2
+111
View File
@@ -0,0 +1,111 @@
/**
* Minimal diff(1) for Bare OS: compares two text files (VFS paths).
* Supports -q (quiet, exit status only), -s (report when identical), -u (single-hunk unified).
*/
/**
* @param {string} a
* @param {string} b
*/
function splitLines(a) {
const s = a.replace(/\r\n/g, '\n')
const parts = s.split('\n')
if (parts.length && parts[parts.length - 1] === '') parts.pop()
return parts
}
/**
* @param {string} pathA
* @param {string} pathB
* @param {string} sa
* @param {string} sb
* @returns {string | null} unified hunk or null if identical
*/
function unifiedOneHunk(pathA, pathB, sa, sb) {
const la = splitLines(sa)
const lb = splitLines(sb)
let i = 0
const n = Math.min(la.length, lb.length)
while (i < n && la[i] === lb[i]) i++
if (i === la.length && i === lb.length) return null
const oldLine = la[i] != null ? la[i] : ''
const newLine = lb[i] != null ? lb[i] : ''
return (
`--- ${pathA}\n+++ ${pathB}\n@@ -${i + 1},1 +${i + 1},1 @@\n-${oldLine}\n+${newLine}\n`
)
}
async function run(ctx, argv) {
let quiet = false
let sameReport = false
let unified = false
/** @type {string[]} */
const files = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--') {
files.push(...argv.slice(i + 1))
break
}
if (a === '-q') {
quiet = true
continue
}
if (a === '-s') {
sameReport = true
continue
}
if (a === '-u' || a === '-U0' || a === '--unified') {
unified = true
continue
}
if (a.startsWith('-')) {
ctx.console.error('diff: unsupported option ' + a)
ctx.exitCode = 2
return
}
files.push(a)
}
if (files.length !== 2) {
ctx.console.error('usage: diff [-q] [-s] [-u] FILE1 FILE2')
ctx.exitCode = 2
return
}
const [f1, f2] = files
let b1
let b2
try {
b1 = await ctx.vfs.readFile(f1)
b2 = await ctx.vfs.readFile(f2)
} catch (e) {
ctx.console.error('diff: ' + ((e && e.message) || String(e)))
ctx.exitCode = 2
return
}
const s1 = b1 ? ctx.b4a.toString(b1) : ''
const s2 = b2 ? ctx.b4a.toString(b2) : ''
if (s1 === s2) {
if (sameReport) ctx.console.log(`Files ${f1} and ${f2} are identical`)
ctx.exitCode = 0
return
}
if (quiet) {
ctx.exitCode = 1
return
}
if (unified) {
const u = unifiedOneHunk(f1, f2, s1, s2)
if (u) ctx.console.log(u.replace(/\n$/, ''))
} else {
const la = splitLines(s1)
const lb = splitLines(s2)
let i = 0
const n = Math.min(la.length, lb.length)
while (i < n && la[i] === lb[i]) i++
ctx.console.log(`${i + 1}c${i + 1}`)
ctx.console.log(`< ${la[i] != null ? la[i] : ''}`)
ctx.console.log('---')
ctx.console.log(`> ${lb[i] != null ? lb[i] : ''}`)
}
ctx.exitCode = 1
}
+75
View File
@@ -40,6 +40,57 @@ function findMatchMtime(st, spec) {
return days >= spec.n && days < spec.n + 1
}
/**
* Device id sketch for `find -xdev` (do not cross `/mnt/<label>` boundaries vs system/personal).
* @param {Record<string, unknown>} ctx
* @param {string} absLogical
*/
function findRouteDevId(ctx, absLogical) {
const a = String(absLogical).replace(/\/+$/, '') || '/'
if (a.startsWith('/mnt/')) {
const label = a.slice(5).split('/').filter(Boolean)[0]
return label ? `mnt:${label}` : 'mnt:'
}
if (a === '/mnt') return 'mnt-root'
const vfs = ctx.vfs
if (!vfs || typeof vfs.route !== 'function') return 'unknown'
const r = vfs.route(absLogical)
if (r.virtualPseudo) return 'pseudo'
if (r.virtualHomeDir) return 'personal-home'
if (r.virtualVarRoot) return 'personal-var'
if (a.startsWith('/tmp')) return 'personal'
if (a.startsWith('/home/')) return 'personal'
if (a.startsWith('/var/')) return 'personal'
return 'system'
}
/** @param {string} s */
function parsePermSpec(s) {
const t = String(s).trim()
if (t.startsWith('/')) return null
let body = t
let allBitsSet = false
if (t.startsWith('-')) {
allBitsSet = true
body = t.slice(1)
}
if (!/^[0-7]{1,4}$/.test(body)) return null
const mask = Number.parseInt(body, 8) & 0o7777
return { allBitsSet, mask }
}
/** @param {Record<string, unknown>} st @param {{ allBitsSet: boolean, mask: number }} spec */
function findMatchPerm(st, spec) {
const mode =
typeof st.mode === 'number'
? st.mode & 0o7777
: typeof st.mode === 'string'
? Number.parseInt(String(st.mode), 8) & 0o7777
: 0
if (spec.allBitsSet) return (mode & spec.mask) === spec.mask
return mode === spec.mask
}
async function findIsEmpty(ctx, path, st) {
if (st.type === 'file' || st.type === 'symlink') return (st.size || 0) === 0
if (st.type !== 'directory') return false
@@ -77,6 +128,7 @@ async function walk(ctx, dir, o, curDepth) {
const nameOk = !o.nameRe || o.nameRe.test(n)
let match = pathOk && nameOk && (!o.wantType || st.type === o.wantType)
if (match && o.mtimeSpec) match = match && findMatchMtime(st, o.mtimeSpec)
if (match && o.permSpec) match = match && findMatchPerm(st, o.permSpec)
if (match && o.newerThanMs != null)
match = match && st.mtimeMs > o.newerThanMs
if (match && o.wantEmpty) {
@@ -141,6 +193,10 @@ async function walk(ctx, dir, o, curDepth) {
}
}
if (st.type !== 'directory') continue
if (o.xdevRootDev != null) {
const subDev = findRouteDevId(ctx, path)
if (subDev !== o.xdevRootDev) continue
}
const absPath = ctx.vfs.resolveLogical(path)
if (o.pruneAbs && absPath === o.pruneAbs) continue
await walk(ctx, path, o, curDepth + 1)
@@ -160,6 +216,9 @@ async function run(ctx, argv) {
let print0 = false
/** @type {{ op: string, n: number } | null} */
let mtimeSpec = null
/** @type {{ allBitsSet: boolean, mask: number } | null} */
let permSpec = null
let xdev = false
/** @type {string | null} */
let newerPath = null
/** @type {string | null} */
@@ -217,6 +276,19 @@ async function run(ctx, argv) {
}
continue
}
if (a === '-perm' && argv[i + 1]) {
permSpec = parsePermSpec(argv[++i])
if (!permSpec) {
ctx.console.error('find: invalid -perm')
ctx.exitCode = 1
return
}
continue
}
if (a === '-xdev') {
xdev = true
continue
}
if (a === '-newer' && argv[i + 1]) {
newerPath = argv[++i]
continue
@@ -336,6 +408,7 @@ async function run(ctx, argv) {
if (Number.isFinite(n)) execMax = Math.min(4096, Math.max(1, n))
}
const abs = ctx.vfs.resolveLogical(root)
const xdevRootDev = xdev ? findRouteDevId(ctx, abs) : null
const o = {
maxDepth,
minDepth,
@@ -344,6 +417,8 @@ async function run(ctx, argv) {
wantType,
print0,
mtimeSpec,
permSpec,
xdevRootDev,
newerThanMs,
pruneAbs,
wantEmpty,
+40 -6
View File
@@ -1,6 +1,7 @@
/**
* Subset of POSIX.1-2017 getconf — fixed values for Bare OS (no host sysconf).
* Unknown names exit with status 1 (matches common getconf for invalid var).
* Live pathconf: **`getconf NAME /absolute/path`** delegates to **`ctx.bareOsPathconf`** when set.
*/
const CONF = {
@@ -41,20 +42,31 @@ const CONF = {
_PC_NAME_MAX: '255',
/** Comma-separated `ctx.bareOsSyscall` op names implemented in stock booter. */
BARE_OS_SYSCALL_OPS:
'readFile,writeFile,readdir,mkdir,stat,unlink,chmod,chdir,getcwd,readlink,symlink,exists,lstat,rmdir,mount,umount,kill,rename,link,access,utimes,truncate,ftruncate,pathconf',
'readFile,writeFile,readdir,mkdir,stat,unlink,chmod,chdir,getcwd,readlink,symlink,exists,lstat,rmdir,mount,umount,kill,rename,link,access,utimes,truncate,ftruncate,fsync,fdatasync,pathconf',
/** Encodings accepted by `/bin/iconv` (subset; case-insensitive names). */
BARE_OS_ICONV_ENCODINGS: 'UTF-8,ISO-8859-1,UTF-16LE,UTF-16BE',
/** Synthetic process table JSON path (logical VFS). */
BARE_OS_PROC_PROCESS_TABLE: '/proc/bare_os/process_table.json'
BARE_OS_PROC_PROCESS_TABLE: '/proc/bare_os/process_table.json',
/** Sidecar suffix for synthetic POSIX ACL text (`getfacl` / `setfacl`). */
BARE_OS_ACL_SIDECAR_SUFFIX: '.bare_acl',
/** Sidecar suffix for extended-attribute JSON (`xattr`). */
BARE_OS_XATTR_SIDECAR_SUFFIX: '.bare_xattr.json',
/** Incremental kernel.ext.d reload after boot (`ctx.bareOsReloadKernelExtDropinsSafe`); 0/1 hint only. */
BARE_OS_KERNEL_EXT_D_HOT_RELOAD: '0',
/** Operator hint for hyperblob-style dedup in host pipelines; guest VFS does not enable automatically. */
BARE_OS_VFS_HYPERBLOBS_DEDUP: '0',
/** This binary: fixed catalog. Use `getconf NAME /path` + `ctx.bareOsPathconf` for live pathconf. */
BARE_OS_GETCONF_SOURCE: 'static_catalog'
}
async function run(ctx, argv) {
const args = argv.slice(1).filter((a) => a !== '--')
let dumpAll = false
let name = null
/** @type {string[]} */
const positional = []
for (const a of args) {
if (a === '-a') dumpAll = true
else if (!a.startsWith('-')) name = a
else if (!a.startsWith('-')) positional.push(a)
else {
ctx.console.error('getconf: unknown option: ' + a)
ctx.exitCode = 1
@@ -70,12 +82,34 @@ async function run(ctx, argv) {
return
}
if (!name) {
ctx.console.error('usage: getconf [-a] system_var')
if (positional.length === 2) {
const varName = positional[0]
const pathSpec = positional[1]
if (
typeof ctx.bareOsPathconf === 'function' &&
pathSpec.startsWith('/')
) {
try {
const v = ctx.bareOsPathconf(pathSpec, varName)
ctx.console.log(String(v))
ctx.exitCode = 0
return
} catch (e) {
ctx.console.error('getconf: ' + (e?.message || String(e)))
ctx.exitCode = 1
return
}
}
}
if (positional.length !== 1) {
ctx.console.error('usage: getconf [-a] system_var [path_for_pathconf]')
ctx.exitCode = 1
return
}
const name = positional[0]
if (Object.prototype.hasOwnProperty.call(CONF, name)) {
ctx.console.log(CONF[name])
ctx.exitCode = 0
+81
View File
@@ -0,0 +1,81 @@
/**
* getfacl — show POSIX-style ACL text for a VFS path.
* When PATH.bare_acl exists, prints that file; otherwise synthesizes user/group/other
* triples from the path mode bits (see setfacl / handbook).
*/
/**
* @param {number} mode
* @param {number} shift
*/
function tripleFromMode(mode, shift) {
const b = (mode >> shift) & 7
const r = b & 4 ? 'r' : '-'
const w = b & 2 ? 'w' : '-'
const x = b & 1 ? 'x' : '-'
return r + w + x
}
async function run(ctx, argv) {
let compact = false
/** @type {string[]} */
const files = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-c' || a === '--compact') {
compact = true
continue
}
if (a === '--help' || a === '-h') {
ctx.console.log('usage: getfacl [-c] FILE')
return
}
if (a.startsWith('-')) {
ctx.console.error('getfacl: unsupported option ' + a)
ctx.exitCode = 1
return
}
files.push(a)
}
if (files.length !== 1) {
ctx.console.error('usage: getfacl [-c] FILE')
ctx.exitCode = 1
return
}
const path = files[0]
const side = path + '.bare_acl'
try {
const st = await ctx.vfs.stat(path)
if (!st) {
ctx.console.error('getfacl: ' + path + ': No such file')
ctx.exitCode = 1
return
}
const raw = await ctx.vfs.readFile(side)
if (raw && raw.byteLength) {
const text = ctx.b4a.toString(raw)
ctx.console.log(text.replace(/\n$/, ''))
return
}
const mode = (st.mode || 0) & 0o777
const u = tripleFromMode(mode, 6)
const g = tripleFromMode(mode, 3)
const o = tripleFromMode(mode, 0)
const lines = compact
? ['user::' + u, 'group::' + g, 'other::' + o]
: [
'# file: ' + path,
'# owner: synthetic',
'# group: synthetic',
'user::' + u,
'group::' + g,
'other::' + o
]
ctx.console.log(lines.join('\n'))
} catch (e) {
ctx.console.error(
'getfacl: ' + path + ': ' + (e && e.message ? e.message : String(e))
)
ctx.exitCode = 1
}
}
@@ -8,8 +8,8 @@ async function readUtf8(ctx, path) {
}
async function run(ctx, argv) {
const a = argv[2]
const b = argv[3]
const a = argv[1]
const b = argv[2]
if (!a || !b) {
ctx.console.error('Usage: kernel-boot-diff FILE1 FILE2')
ctx.console.error('Compares NDJSON or line-oriented boot checkpoint dumps.')
+9
View File
@@ -1,3 +1,12 @@
/**
* Move/rename via copy + delete. Hyperdrive has no single-key rename across paths, so
* directory trees and cross-location moves are duplicated then removed. A single regular
* file to a new non-directory path uses read + write + unlink when detected below.
*
* Documented limitations: cross-volume moves always copy+delete; EXDEV-style behavior is
* implicit. Busy targets, partial copy failures, and union read-only trees surface as
* generic errors from the VFS. Prefer same-directory renames for smallest blast radius.
*/
async function mvCopyPath(ctx, from, to, recursive, followSymlink) {
const st = await ctx.vfs.lstat(from)
if (!st) return false
+127
View File
@@ -0,0 +1,127 @@
/**
* Minimal patch(1): applies a single unified diff from stdin (ctx.shellStdin).
* Supports -pNUM strip, --dry-run. Intended for diffs emitted by Bare OS diff -u.
*/
/**
* @param {string} path
* @param {number} p
*/
function stripPath(path, p) {
const segs = path.replace(/\\/g, '/').split('/').filter(Boolean)
const rest = segs.slice(Math.min(p, segs.length))
return rest.length ? rest.join('/') : segs[segs.length - 1] || path
}
async function run(ctx, argv) {
let p = 0
let dry = false
/** @type {string | null} */
let overrideFile = null
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--dry-run' || a === '--check') {
dry = true
continue
}
if (a === '-p' || a === '--strip') {
const n = argv[i + 1]
if (n == null || !/^\d+$/.test(n)) {
ctx.console.error('patch: -p requires a number')
ctx.exitCode = 2
return
}
p = Number(n)
i++
continue
}
if (a.startsWith('-p') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
p = Number(a.slice(2))
continue
}
if (a.startsWith('-')) {
ctx.console.error('patch: unsupported option ' + a)
ctx.exitCode = 2
return
}
overrideFile = a
break
}
const raw = bareStdin(ctx) || ''
const lines = raw.replace(/\r\n/g, '\n').split('\n')
let minus = ''
let plus = ''
for (let i = 0; i < lines.length; i++) {
const ln = lines[i]
if (ln.startsWith('--- ')) {
minus = ln.slice(4).trim().split(/\s+/)[0]
continue
}
if (ln.startsWith('+++ ')) {
plus = ln.slice(4).trim().split(/\s+/)[0]
continue
}
}
const targetRaw = overrideFile || plus || minus
if (!targetRaw) {
ctx.console.error('patch: could not determine path from patch')
ctx.exitCode = 2
return
}
const target = stripPath(targetRaw.replace(/^b\//, ''), p)
let hunkStart = lines.findIndex((l) => /^@@/.test(l))
if (hunkStart < 0) {
ctx.console.error('patch: missing @@ hunk')
ctx.exitCode = 2
return
}
/** @type {string[]} */
const oldParts = []
/** @type {string[]} */
const newParts = []
for (let i = hunkStart + 1; i < lines.length; i++) {
const l = lines[i]
if (l.startsWith('@@')) break
if (l.startsWith('-')) oldParts.push(l.slice(1))
else if (l.startsWith('+')) newParts.push(l.slice(1))
else if (l.startsWith(' ')) {
const body = l.slice(1)
oldParts.push(body)
newParts.push(body)
}
}
let cur = ''
try {
const buf = await ctx.vfs.readFile(target)
cur = buf ? ctx.b4a.toString(buf) : ''
} catch (e) {
ctx.console.error('patch: ' + ((e && e.message) || String(e)))
ctx.exitCode = 2
return
}
const cl = splitLinesKeep(cur)
const expectOld = oldParts.join('\n')
const gotOld = cl.join('\n')
if (expectOld !== gotOld) {
ctx.console.error('patch: file content does not match hunk (try correct -p)')
ctx.exitCode = 1
return
}
const out = newParts.join('\n') + (newParts.length ? '\n' : '')
if (!dry) await ctx.vfs.writeFile(target, ctx.b4a.from(out))
ctx.exitCode = 0
}
/** @param {string} s */
function splitLinesKeep(s) {
const t = s.replace(/\r\n/g, '\n')
const parts = t.split('\n')
if (parts.length && parts[parts.length - 1] === '') parts.pop()
return parts
}
+25 -5
View File
@@ -1,3 +1,27 @@
/** Unescape \\n \\t \\r \\\\ and octal \\ddd inside FORMAT (POSIX-style subset). */
function barePrintfUnescapeFormat(fmt) {
let o = ''
for (let i = 0; i < fmt.length; i++) {
if (fmt[i] !== '\\') {
o += fmt[i]
continue
}
const c = fmt[++i]
if (c === undefined) break
if (c === 'n') o += '\n'
else if (c === 't') o += '\t'
else if (c === 'r') o += '\r'
else if (c === '\\') o += '\\'
else if (c >= '0' && c <= '7') {
let oct = c
while (i + 1 < fmt.length && /[0-7]/.test(fmt[i + 1]) && oct.length < 3)
oct += fmt[++i]
o += String.fromCharCode(Number.parseInt(oct, 8) & 0xff)
} else o += c
}
return o
}
/** @param {string} s */
function barePrintfBackslashArg(s) {
let o = ''
@@ -66,10 +90,6 @@ async function run(ctx, argv) {
}
const fmt = argv[1]
const args = argv.slice(2)
let unescaped = fmt
unescaped = unescaped.replace(/\\n/g, '\n')
unescaped = unescaped.replace(/\\t/g, '\t')
unescaped = unescaped.replace(/\\r/g, '\r')
unescaped = unescaped.replace(/\\\\/g, '\\')
const unescaped = barePrintfUnescapeFormat(fmt)
ctx.console.log(barePrintfFormat(unescaped, args))
}
+63
View File
@@ -0,0 +1,63 @@
/**
* setfacl — store or clear synthetic ACL sidecar text (PATH.bare_acl).
* Without -b, reads ACL lines from ctx.shellStdin (same pattern as patch).
*/
async function run(ctx, argv) {
let clear = false
/** @type {string[]} */
const files = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-b' || a === '--remove-all') {
clear = true
continue
}
if (a === '--help' || a === '-h') {
ctx.console.log(
'usage: setfacl [-b] PATH\nWrites ACL text from stdin when not clearing.'
)
return
}
if (a.startsWith('-')) {
ctx.console.error('setfacl: unsupported option ' + a)
ctx.exitCode = 1
return
}
files.push(a)
}
if (files.length !== 1) {
ctx.console.error('usage: setfacl [-b] PATH')
ctx.exitCode = 1
return
}
const path = files[0]
const side = path + '.bare_acl'
try {
const st = await ctx.vfs.stat(path)
if (!st) {
ctx.console.error('setfacl: ' + path + ': No such file')
ctx.exitCode = 1
return
}
if (clear) {
try {
await ctx.vfs.unlink(side)
} catch {
/* ignore missing sidecar */
}
return
}
const text = bareStdin(ctx)
if (!text || !String(text).trim()) {
ctx.console.error('setfacl: ACL text required on stdin')
ctx.exitCode = 1
return
}
const body = String(text).endsWith('\n') ? String(text) : String(text) + '\n'
await ctx.vfs.writeFile(side, ctx.b4a.from(body))
} catch (e) {
ctx.console.error('setfacl: ' + (e && e.message ? e.message : String(e)))
ctx.exitCode = 1
}
}
+41
View File
@@ -0,0 +1,41 @@
async function run(ctx, argv) {
const script = argv[1]
if (script == null) {
ctx.console.error('usage: sh SCRIPT')
ctx.exitCode = 2
return
}
if (typeof ctx.execLine !== 'function') {
ctx.console.error('sh: execLine is not available (requires a Bare OS booter session)')
ctx.exitCode = 1
return
}
let buf
try {
buf = await ctx.vfs.readFile(script)
} catch (e) {
ctx.console.error('sh: ' + ((e && e.message) || String(e)))
ctx.exitCode = 1
return
}
if (buf == null) {
ctx.console.error('sh: ' + script + ': not found')
ctx.exitCode = 127
return
}
let text = ctx.b4a.toString(buf)
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1)
text = text.replace(/^\ufeff/, '')
if (text.startsWith('#!')) {
const nl = text.indexOf('\n')
text = nl === -1 ? '' : text.slice(nl + 1)
}
const lines = text.split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim()
if (!trimmed || trimmed.startsWith('#')) continue
await ctx.execLine(trimmed)
if ((Number(ctx.exitCode) || 0) !== 0) return
}
ctx.exitCode = 0
}
+40 -3
View File
@@ -46,6 +46,41 @@ function sortKeyObj(o) {
return { n: 0, raw: keyText }
}
/**
* @param {Record<string, unknown> | undefined} ctx
*/
function sortLocaleTag(ctx) {
const env =
ctx && ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object'
? ctx.vfs.env
: ctx && ctx.env && typeof ctx.env === 'object'
? ctx.env
: {}
const t = String(
/** @type {Record<string, string>} */ (env).LC_ALL ||
/** @type {Record<string, string>} */ (env).LC_COLLATE ||
'C'
).trim()
return t || 'C'
}
/**
* @param {Record<string, unknown> | undefined} ctx
* @param {string} a
* @param {string} b
*/
function sortLocaleCompareRaw(ctx, a, b) {
const tag = sortLocaleTag(ctx)
if (tag === 'C' || tag === 'POSIX') {
return a < b ? -1 : a > b ? 1 : 0
}
try {
return String(a).localeCompare(String(b), tag, { sensitivity: 'variant' })
} catch {
return a < b ? -1 : a > b ? 1 : 0
}
}
/**
* @param {string} x
* @param {string} y
@@ -56,6 +91,7 @@ function sortKeyObj(o) {
* @param {number | null} opts.keyStart
* @param {number | null} opts.keyEnd
* @param {string | null} opts.dForKey
* @param {Record<string, unknown> | undefined} [opts.ctx]
* @returns {number}
*/
function sortCompareLines(x, y, opts) {
@@ -74,8 +110,8 @@ function sortCompareLines(x, y, opts) {
return opts.reverse ? -ord : ord
}
}
const cmp =
kx.raw < ky.raw ? -1 : kx.raw > ky.raw ? 1 : x < y ? -1 : x > y ? 1 : 0
let cmp = sortLocaleCompareRaw(opts.ctx, kx.raw, ky.raw)
if (cmp === 0) cmp = sortLocaleCompareRaw(opts.ctx, x, y)
return opts.reverse ? -cmp : cmp
}
@@ -290,7 +326,8 @@ async function run(ctx, argv) {
fold,
keyStart,
keyEnd,
dForKey
dForKey,
ctx
}
if (checkMode !== 'off') {
+38
View File
@@ -1,3 +1,32 @@
/** @param {Record<string, unknown>} ctx */
function testParseEuidEgid(ctx) {
const e = (ctx.vfs && ctx.vfs.env) || ctx.env || {}
const uid = Number.parseInt(String(e.UID != null ? e.UID : '1000'), 10)
const gid = Number.parseInt(String(e.GID != null ? e.GID : '1000'), 10)
return {
euid: Number.isFinite(uid) ? uid : 1000,
egid: Number.isFinite(gid) ? gid : 1000
}
}
/**
* User/group/other permission triplet (07) for the effective uid/gid.
* @param {Record<string, unknown>} st
* @param {number} euid
* @param {number} egid
*/
function testEffPermTriplet(st, euid, egid) {
const mode =
typeof st.mode === 'number'
? st.mode & 0o777
: Number.parseInt(String(st.mode || '644'), 8) & 0o777
const fuid = st.uid != null ? Number(st.uid) : 0
const fgid = st.gid != null ? Number(st.gid) : 0
if (euid === fuid) return (mode >> 6) & 7
if (egid === fgid) return (mode >> 3) & 7
return mode & 7
}
async function evalTest(ctx, args) {
if (!args.length) return false
if (args[0] === '!') {
@@ -38,10 +67,19 @@ async function evalTest(ctx, args) {
const st = await ctx.vfs.lstat(p)
return st != null && st.type === 'symlink'
}
const { euid, egid } = testParseEuidEgid(ctx)
const st = await ctx.vfs.stat(p)
if (op === '-e' || op === '-a') return st != null
if (op === '-f') return st != null && st.type === 'file'
if (op === '-d') return st != null && st.type === 'directory'
if (op === '-r')
return st != null && (testEffPermTriplet(st, euid, egid) & 4) !== 0
if (op === '-w')
return st != null && (testEffPermTriplet(st, euid, egid) & 2) !== 0
if (op === '-x')
return st != null && (testEffPermTriplet(st, euid, egid) & 1) !== 0
if (op === '-s')
return st != null && st.type === 'file' && Number(st.size) > 0
if (op === '-z') return p.length === 0
if (op === '-n') return p.length > 0
return false
+64 -26
View File
@@ -3,14 +3,27 @@
* Limits: stdin 256KiB, 4096 whitespace/null tokens, 128 args per invocation,
* 64 invocations per run. Exceeding limits is a fatal error (exit 125).
* Supports -0/--null, -n, -I repl (replace repl in utility argv; implies -n 1 unless -n given).
* -P N is accepted; stock booter runs sequentially; N is capped at MAX_P_FLAG (4).
* -P N runs up to N batches in parallel (each batch uses a shallow ctx clone so exitCode does not race).
* Max -P is min(requested, BARE_OS_XARGS_MAX_PROCS env, 32); default cap 8 when env unset.
*/
const MAX_STDIN = 256 * 1024
const MAX_TOKENS = 4096
const MAX_PER_INVOCATION = 128
const MAX_INVOCATIONS = 64
const MAX_P_FLAG = 4
const DEFAULT_P_CAP = 8
const ABS_P_CAP = 32
/**
* @param {Record<string, unknown>} ctx
*/
function maxParallelFromEnv(ctx) {
const env = (ctx && ctx.vfs && ctx.vfs.env) || (ctx && ctx.env) || {}
const raw = String(env.BARE_OS_XARGS_MAX_PROCS || '').trim()
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_P_CAP
const n = Number(raw)
return Math.min(ABS_P_CAP, Math.max(1, n))
}
async function run(ctx, argv) {
if (typeof ctx.runBinCommand !== 'function') {
@@ -26,6 +39,7 @@ async function run(ctx, argv) {
/** @type {string | null} */
let repl = null
let nExplicit = false
const envPCap = maxParallelFromEnv(ctx)
let pCap = 1
let i = 0
@@ -48,14 +62,16 @@ async function run(ctx, argv) {
return
}
const raw = Number(n)
pCap = Math.min(MAX_P_FLAG, Math.max(1, raw))
if (raw > MAX_P_FLAG) {
pCap = Math.min(envPCap, Math.max(1, raw))
if (raw > envPCap) {
ctx.console.error(
'xargs: -P ' +
raw +
' exceeds Bare OS cap ' +
MAX_P_FLAG +
' (parallelism hint only; sequential execution)'
' exceeds cap ' +
envPCap +
' (raise BARE_OS_XARGS_MAX_PROCS up to ' +
ABS_P_CAP +
')'
)
}
i += 2
@@ -63,12 +79,14 @@ async function run(ctx, argv) {
}
if (a.startsWith('-P') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
const raw = Number(a.slice(2))
pCap = Math.min(MAX_P_FLAG, Math.max(1, raw))
if (raw > MAX_P_FLAG) {
pCap = Math.min(envPCap, Math.max(1, raw))
if (raw > envPCap) {
ctx.console.error(
'xargs: -P exceeds Bare OS cap ' +
MAX_P_FLAG +
' (parallelism hint only; sequential execution)'
'xargs: -P exceeds cap ' +
envPCap +
' (raise BARE_OS_XARGS_MAX_PROCS up to ' +
ABS_P_CAP +
')'
)
}
i++
@@ -113,9 +131,9 @@ async function run(ctx, argv) {
ctx.console.error(
'xargs: Bare OS supports: -0/--null, -n N (max ' +
MAX_PER_INVOCATION +
' per run), -I repl, -P N (max ' +
MAX_P_FLAG +
', sequential)'
' per run), -I repl, -P N (parallel batches, cap ' +
envPCap +
')'
)
ctx.exitCode = 1
return
@@ -124,7 +142,6 @@ async function run(ctx, argv) {
/** @type {string[]} */
let cmd = args.slice(i)
if (cmd.length === 0) cmd = ['echo']
void pCap
let text = bareStdin(ctx) || ''
if (text.length > MAX_STDIN) {
@@ -152,9 +169,10 @@ async function run(ctx, argv) {
}
/**
* @param {Record<string, unknown>} target
* @param {string[]} batch
*/
const runOne = async (batch) => {
const runOne = async (target, batch) => {
const subst = batch.join(' ')
/** @type {string[]} */
const toRun =
@@ -169,29 +187,49 @@ async function run(ctx, argv) {
}
return o
})
await ctx.runBinCommand(toRun)
await target.runBinCommand(toRun)
}
if (pieces.length === 0) {
await runOne([])
await runOne(ctx, [])
return
}
let invocations = 0
/** @type {string[][]} */
const batches = []
for (let o = 0; o < pieces.length; o += maxBatch) {
if (++invocations > MAX_INVOCATIONS) {
if (batches.length >= MAX_INVOCATIONS) {
ctx.console.error(
'xargs: exceeded ' + MAX_INVOCATIONS + ' invocations (Bare OS limit)'
)
ctx.exitCode = 125
return
}
const batch = pieces.slice(o, o + maxBatch)
await runOne(batch)
const ec = Number(ctx.exitCode) || 0
if (ec !== 0) {
ctx.exitCode = ec
batches.push(pieces.slice(o, o + maxBatch))
}
if (pCap <= 1) {
for (const batch of batches) {
await runOne(ctx, batch)
if ((Number(ctx.exitCode) || 0) !== 0) return
}
return
}
for (let w = 0; w < batches.length; w += pCap) {
const slice = batches.slice(w, w + pCap)
const codes = await Promise.all(
slice.map(async (batch) => {
const c = Object.assign({}, ctx, { exitCode: 0 })
await runOne(c, batch)
return Number(c.exitCode) || 0
})
)
const bad = codes.find((x) => x !== 0)
if (bad != null) {
ctx.exitCode = bad
return
}
}
ctx.exitCode = 0
}
+149
View File
@@ -0,0 +1,149 @@
/**
* xattr — extended attributes via sidecar JSON (PATH.bare_xattr.json).
* Values are stored base64-encoded UTF-8 strings for JSON safety.
*/
/**
* @param {string} val
*/
function xattrB64Encode(val) {
const s = String(val)
if (typeof globalThis.Buffer !== 'undefined') {
return globalThis.Buffer.from(s, 'utf8').toString('base64')
}
const u8 = new TextEncoder().encode(s)
let bin = ''
for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i])
if (typeof globalThis.btoa === 'function') return globalThis.btoa(bin)
throw new Error('xattr: base64 encode unavailable')
}
/**
* @param {string} b64
*/
function xattrB64Decode(b64) {
const t = String(b64).replace(/\s+/g, '')
if (typeof globalThis.Buffer !== 'undefined') {
return globalThis.Buffer.from(t, 'base64').toString('utf8')
}
if (typeof globalThis.atob === 'function') {
const bin = globalThis.atob(t)
let out = ''
for (let i = 0; i < bin.length; i++) out += bin[i]
return out
}
throw new Error('xattr: base64 decode unavailable')
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
*/
async function readXattrMap(ctx, path) {
const side = path + '.bare_xattr.json'
const raw = await ctx.vfs.readFile(side)
if (!raw || !raw.byteLength) return {}
try {
const o = JSON.parse(ctx.b4a.toString(raw))
return o && typeof o === 'object' && !Array.isArray(o) ? o : {}
} catch {
return {}
}
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
* @param {Record<string, string>} obj
*/
async function writeXattrMap(ctx, path, obj) {
const side = path + '.bare_xattr.json'
const text = JSON.stringify(obj) + '\n'
await ctx.vfs.writeFile(side, ctx.b4a.from(text))
}
async function run(ctx, argv) {
let list = false
/** @type {string | null} */
let delName = null
/** @type {{ name: string, val: string } | null} */
let writePair = null
/** @type {string[]} */
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-l' || a === '--list') {
list = true
continue
}
if (a === '-w' || a === '--write') {
const name = argv[i + 1]
const val = argv[i + 2]
if (!name || val == null) {
ctx.console.error('xattr: -w requires NAME VALUE')
ctx.exitCode = 1
return
}
writePair = { name, val }
i += 2
continue
}
if (a === '-d' || a === '--delete') {
const name = argv[i + 1]
if (!name) {
ctx.console.error('xattr: -d requires NAME')
ctx.exitCode = 1
return
}
delName = name
i++
continue
}
if (a === '--help' || a === '-h') {
ctx.console.log('usage: xattr [-l] [-w NAME VALUE] [-d NAME] PATH')
return
}
if (a.startsWith('-')) {
ctx.console.error('xattr: unsupported option ' + a)
ctx.exitCode = 1
return
}
rest.push(a)
}
if (rest.length !== 1) {
ctx.console.error('usage: xattr [-l] [-w NAME VALUE] [-d NAME] PATH')
ctx.exitCode = 1
return
}
const path = rest[0]
try {
const st = await ctx.vfs.stat(path)
if (!st) {
ctx.console.error('xattr: ' + path + ': No such file')
ctx.exitCode = 1
return
}
const obj = /** @type {Record<string, string>} */ (await readXattrMap(ctx, path))
if (delName) {
delete obj[delName]
await writeXattrMap(ctx, path, obj)
return
}
if (writePair) {
obj[writePair.name] = xattrB64Encode(writePair.val)
await writeXattrMap(ctx, path, obj)
return
}
const keys = Object.keys(obj).sort()
for (const k of keys) {
if (list) {
ctx.console.log(k + ': ' + xattrB64Decode(obj[k]))
} else {
ctx.console.log(k)
}
}
} catch (e) {
ctx.console.error('xattr: ' + (e && e.message ? e.message : String(e)))
ctx.exitCode = 1
}
}
@@ -0,0 +1,27 @@
/**
* CI smoke checks for awk/sed sources (POSIX-oriented coverage is partial by design).
*/
import { readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import test from 'brittle'
const __dirname = dirname(fileURLToPath(import.meta.url))
const srcDir = join(__dirname, '../src')
test('sed.js exists and documents line-oriented operation', async (t) => {
const s = await readFile(join(srcDir, 'sed.js'), 'utf8')
t.ok(s.includes('async function run'))
t.ok(s.length > 200)
})
test('awk.js exists and wires field parsing', async (t) => {
const s = await readFile(join(srcDir, 'awk.js'), 'utf8')
t.ok(s.includes('async function run'))
t.ok(s.includes('field-separator') || s.includes('split'))
})
test('printf FORMAT octal escape helper is defined', async (t) => {
const s = await readFile(join(srcDir, 'printf.js'), 'utf8')
t.ok(s.includes('barePrintfUnescapeFormat'))
})
@@ -0,0 +1,156 @@
/**
* getfacl / setfacl / xattr against an in-memory vfs stub.
*/
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
/**
* @param {string} name
*/
async function loadBin(name) {
const runtime = await readFile(
path.join(__dirname, '../lib/runtime.js'),
'utf8'
)
const body = await readFile(
path.join(__dirname, `../src/${name}.js`),
'utf8'
)
return new AsyncFunction(
'ctx',
'argv',
`${runtime}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n`
)
}
function createCtx(store) {
const logs = []
const errs = []
return {
b4a,
exitCode: 0,
shellStdin: '',
console: {
log: (m) => logs.push(String(m)),
error: (m) => errs.push(String(m))
},
vfs: {
async stat(p) {
if (!Object.prototype.hasOwnProperty.call(store.files, p)) return null
return store.files[p].stat
},
async readFile(p) {
const ent = store.files[p]
if (!ent) return null
return ent.body
},
async writeFile(p, body) {
store.files[p] = {
stat: { mode: 0o644, type: 'file', mtimeMs: Date.now() },
body: body instanceof Uint8Array ? body : b4a.from(body)
}
},
async unlink(p) {
delete store.files[p]
}
},
_logs: logs,
_errs: errs
}
}
test('getfacl synthesizes mode triples and reads sidecar', async (t) => {
const store = {
files: {
'/a': {
stat: { mode: 0o755, type: 'file', mtimeMs: 1 },
body: null
},
'/b': {
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
body: null
},
'/b.bare_acl': {
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
body: b4a.from('user::rw-\n')
}
}
}
const runA = await loadBin('getfacl')
const ctxA = createCtx(store)
await runA(ctxA, ['getfacl', '-c', '/a'])
t.is(ctxA.exitCode, 0)
t.is(ctxA._logs.join('\n'), 'user::rwx\ngroup::r-x\nother::r-x')
const runB = await loadBin('getfacl')
const ctxB = createCtx(store)
await runB(ctxB, ['getfacl', '/b'])
t.is(ctxB.exitCode, 0)
t.ok(ctxB._logs[0].includes('user::rw-'))
})
test('setfacl -b and stdin write', async (t) => {
const store = {
files: {
'/x': {
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
body: null
},
'/x.bare_acl': {
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
body: b4a.from('old\n')
}
}
}
const runClear = await loadBin('setfacl')
const ctx1 = createCtx(store)
await runClear(ctx1, ['setfacl', '-b', '/x'])
t.is(ctx1.exitCode, 0)
t.absent(store.files['/x.bare_acl'])
const runSet = await loadBin('setfacl')
const ctx2 = createCtx(store)
ctx2.shellStdin = 'user::r--\n'
await runSet(ctx2, ['setfacl', '/x'])
t.is(ctx2.exitCode, 0)
t.is(b4a.toString(store.files['/x.bare_acl'].body), 'user::r--\n')
})
test('xattr -w -l -d round trip', async (t) => {
const store = {
files: {
'/f': {
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
body: null
}
}
}
const runW = await loadBin('xattr')
const ctxW = createCtx(store)
await runW(ctxW, ['xattr', '-w', 'user.t', 'hi', '/f'])
t.is(ctxW.exitCode, 0)
const runL = await loadBin('xattr')
const ctxL = createCtx(store)
await runL(ctxL, ['xattr', '-l', '/f'])
t.is(ctxL.exitCode, 0)
t.is(ctxL._logs.join('\n'), 'user.t: hi')
const runD = await loadBin('xattr')
const ctxD = createCtx(store)
await runD(ctxD, ['xattr', '-d', 'user.t', '/f'])
t.is(ctxD.exitCode, 0)
const runNames = await loadBin('xattr')
const ctxN = createCtx(store)
await runNames(ctxN, ['xattr', '/f'])
t.is(ctxN.exitCode, 0)
t.is(ctxN._logs.length, 0)
})