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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 (0–7) 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user