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.
524 lines
14 KiB
Plaintext
524 lines
14 KiB
Plaintext
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
|
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
|
function bareStdin(ctx) {
|
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
|
}
|
|
|
|
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
|
function bareFormatModeString(mode, type) {
|
|
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
|
const perm = mode & 0o777
|
|
const r = (bit) => (perm & bit ? 'r' : '-')
|
|
const w = (bit) => (perm & bit ? 'w' : '-')
|
|
const x = (bit) => (perm & bit ? 'x' : '-')
|
|
return (
|
|
typeChar +
|
|
r(0o400) +
|
|
w(0o200) +
|
|
x(0o100) +
|
|
r(0o040) +
|
|
w(0o020) +
|
|
x(0o010) +
|
|
r(0o004) +
|
|
w(0o002) +
|
|
x(0o001)
|
|
)
|
|
}
|
|
|
|
/** @param {number} mtimeMs @param {number} [nowMs] */
|
|
function bareFormatLsMtime(mtimeMs, nowMs) {
|
|
const now = nowMs != null ? nowMs : Date.now()
|
|
const d = new Date(mtimeMs)
|
|
const months = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec'
|
|
]
|
|
const mon = months[d.getMonth()]
|
|
const day = String(d.getDate()).padStart(2, ' ')
|
|
const sixMo = 180 * 24 * 3600 * 1000
|
|
if (Math.abs(now - mtimeMs) > sixMo) {
|
|
const yr = String(d.getFullYear()).padStart(4, ' ')
|
|
return mon + ' ' + day + ' ' + yr
|
|
}
|
|
const hh = String(d.getHours()).padStart(2, '0')
|
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
return mon + ' ' + day + ' ' + hh + ':' + mm
|
|
}
|
|
|
|
/** @param {number} size */
|
|
function barePosixBlocks(size) {
|
|
return Math.ceil(Number(size) / 512) || 0
|
|
}
|
|
|
|
/**
|
|
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
|
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string | Uint8Array} chunk
|
|
* @returns {boolean}
|
|
*/
|
|
function bareOsEmitRaw(ctx, chunk) {
|
|
if (typeof ctx.bareOsBinWrite === 'function') {
|
|
const b4 = ctx.b4a
|
|
const u8 =
|
|
typeof chunk === 'string'
|
|
? b4 && typeof b4.from === 'function'
|
|
? b4.from(chunk)
|
|
: new TextEncoder().encode(chunk)
|
|
: chunk
|
|
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
|
return true
|
|
}
|
|
const w = globalThis.process?.stdout?.write
|
|
if (typeof w === 'function') {
|
|
w.call(globalThis.process.stdout, chunk)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* curDepth is distance from the search root directory (0 at the initial path).
|
|
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
|
|
*/
|
|
function findEmitLine(ctx, path, print0) {
|
|
if (!print0) {
|
|
ctx.console.log(path)
|
|
return
|
|
}
|
|
if (!bareOsEmitRaw(ctx, path + '\0')) {
|
|
ctx.console.error(
|
|
'find: -print0 requires process.stdout.write or ctx.bareOsBinWrite'
|
|
)
|
|
ctx.exitCode = 1
|
|
}
|
|
}
|
|
|
|
/** @param {string} s */
|
|
function parseMtimeSpec(s) {
|
|
const t = String(s)
|
|
let op = ''
|
|
let num = t
|
|
if (t.startsWith('+')) {
|
|
op = '+'
|
|
num = t.slice(1)
|
|
} else if (t.startsWith('-') && t.length > 1) {
|
|
op = '-'
|
|
num = t.slice(1)
|
|
}
|
|
const n = Number.parseFloat(num)
|
|
if (!Number.isFinite(n)) return null
|
|
return { op, n }
|
|
}
|
|
|
|
/** @param {{ op: string, n: number }} spec */
|
|
function findMatchMtime(st, spec) {
|
|
const days = (Date.now() - st.mtimeMs) / 86400000
|
|
if (spec.op === '+') return days > spec.n
|
|
if (spec.op === '-') return days < spec.n
|
|
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
|
|
const names = await ctx.vfs.readdir(path)
|
|
const rest = names.filter((n) => n !== '.bareos_empty')
|
|
return rest.length === 0
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} dir
|
|
* @param {Record<string, unknown>} o
|
|
* @param {number} curDepth
|
|
*/
|
|
async function walk(ctx, dir, o, curDepth) {
|
|
if (o.maxDepth >= 0 && curDepth > o.maxDepth) return
|
|
let names
|
|
try {
|
|
names = await ctx.vfs.readdir(dir)
|
|
} catch {
|
|
return
|
|
}
|
|
for (const n of names) {
|
|
if (n === '.bareos_empty') continue
|
|
const path = dir.replace(/\/+$/, '') + '/' + n
|
|
let st
|
|
try {
|
|
st = await ctx.vfs.lstat(path)
|
|
} catch {
|
|
continue
|
|
}
|
|
if (!st) continue
|
|
const gnuDepth = curDepth + 1
|
|
const pathOk = !o.pathRe || o.pathRe.test(path)
|
|
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) {
|
|
match = match && (await findIsEmpty(ctx, path, st))
|
|
}
|
|
if (match && o.regexPath && !o.regexPath.test(path)) match = false
|
|
if (match && gnuDepth >= o.minDepth) {
|
|
if (o.doDelete) {
|
|
if (ctx.vfs.env.BARE_OS_FIND_DELETE !== '1') {
|
|
const stw = /** @type {{ warned?: boolean }} */ (o.deleteState)
|
|
if (!stw.warned) {
|
|
ctx.console.error(
|
|
'find: -delete disabled (set BARE_OS_FIND_DELETE=1 to confirm)'
|
|
)
|
|
stw.warned = true
|
|
}
|
|
ctx.exitCode = 1
|
|
} else {
|
|
try {
|
|
await ctx.vfs.rm(path, { recursive: true, force: true })
|
|
} catch (e) {
|
|
ctx.console.error('find: ' + path + ': ' + (e.message || e))
|
|
ctx.exitCode = 1
|
|
}
|
|
}
|
|
} else if (o.execTemplate && o.execTemplate.length) {
|
|
if (typeof ctx.runBinCommand !== 'function') {
|
|
const stw = /** @type {{ noRun?: boolean }} */ (o.deleteState)
|
|
if (!stw.noRun) {
|
|
ctx.console.error('find: -exec requires ctx.runBinCommand')
|
|
stw.noRun = true
|
|
}
|
|
ctx.exitCode = 1
|
|
} else if (o.execCount >= o.execMax) {
|
|
const stw = /** @type {{ execCap?: boolean }} */ (o.deleteState)
|
|
if (!stw.execCap) {
|
|
ctx.console.error(
|
|
'find: -exec/-ok: invocation limit (' +
|
|
o.execMax +
|
|
') exceeded (raise BARE_OS_FIND_EXEC_MAX)'
|
|
)
|
|
stw.execCap = true
|
|
}
|
|
ctx.exitCode = 1
|
|
} else {
|
|
const ok =
|
|
!o.execUseOk ||
|
|
ctx.vfs.env.BARE_OS_FIND_OK === '1' ||
|
|
ctx.vfs.env.BARE_OS_FIND_OK === 'true'
|
|
if (o.execUseOk && !ok) {
|
|
/* skip without running */
|
|
} else {
|
|
const subst = o.execTemplate.map((arg) =>
|
|
arg === '{}' ? path : arg.split('{}').join(path)
|
|
)
|
|
o.execCount++
|
|
await ctx.runBinCommand(subst)
|
|
}
|
|
}
|
|
} else {
|
|
findEmitLine(ctx, path, o.print0)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
async function run(ctx, argv) {
|
|
let maxDepth = -1
|
|
let minDepth = 1
|
|
/** @type {string | null} */
|
|
let nameGlob = null
|
|
/** @type {string | null} */
|
|
let pathGlob = null
|
|
/** @type {'file' | 'directory' | 'symlink' | null} */
|
|
let wantType = null
|
|
let nameIgnoreCase = false
|
|
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} */
|
|
let prunePath = null
|
|
let wantEmpty = false
|
|
let doDelete = false
|
|
/** @type {string[] | null} */
|
|
let execTemplate = null
|
|
let execUseOk = false
|
|
/** @type {string | null} */
|
|
let regexPathStr = null
|
|
const rest = []
|
|
for (let i = 1; i < argv.length; i++) {
|
|
const a = argv[i]
|
|
if ((a === '-maxdepth' || a === '-depth') && argv[i + 1]) {
|
|
maxDepth = Number.parseInt(argv[++i], 10)
|
|
continue
|
|
}
|
|
if (a === '-mindepth' && argv[i + 1]) {
|
|
minDepth = Number.parseInt(argv[++i], 10)
|
|
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
|
|
continue
|
|
}
|
|
if (a === '-path' && argv[i + 1]) {
|
|
pathGlob = argv[++i]
|
|
continue
|
|
}
|
|
if (a === '-name' && argv[i + 1]) {
|
|
nameGlob = argv[++i]
|
|
nameIgnoreCase = false
|
|
continue
|
|
}
|
|
if (a === '-iname' && argv[i + 1]) {
|
|
nameGlob = argv[++i]
|
|
nameIgnoreCase = true
|
|
continue
|
|
}
|
|
if (a === '-type' && argv[i + 1]) {
|
|
const t = argv[++i]
|
|
if (t === 'f') wantType = 'file'
|
|
else if (t === 'd') wantType = 'directory'
|
|
else if (t === 'l') wantType = 'symlink'
|
|
continue
|
|
}
|
|
if (a === '-print0') {
|
|
print0 = true
|
|
continue
|
|
}
|
|
if (a === '-mtime' && argv[i + 1]) {
|
|
mtimeSpec = parseMtimeSpec(argv[++i])
|
|
if (!mtimeSpec) {
|
|
ctx.console.error('find: invalid -mtime')
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
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
|
|
}
|
|
if (a === '-prune' && argv[i + 1]) {
|
|
prunePath = argv[++i]
|
|
continue
|
|
}
|
|
if (a === '-empty') {
|
|
wantEmpty = true
|
|
continue
|
|
}
|
|
if (a === '-delete') {
|
|
doDelete = true
|
|
continue
|
|
}
|
|
if ((a === '-exec' || a === '-ok') && argv[i + 1]) {
|
|
execUseOk = a === '-ok'
|
|
i++
|
|
const parts = []
|
|
while (i < argv.length && argv[i] !== ';') {
|
|
parts.push(argv[i++])
|
|
}
|
|
if (i >= argv.length || argv[i] !== ';') {
|
|
ctx.console.error('find: ' + a + ' must be terminated with ;')
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
i++
|
|
if (!parts.length) {
|
|
ctx.console.error('find: empty ' + a)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
execTemplate = parts
|
|
continue
|
|
}
|
|
if (a === '-regex' && argv[i + 1]) {
|
|
const pat = argv[++i]
|
|
try {
|
|
new RegExp(pat)
|
|
regexPathStr = pat
|
|
} catch {
|
|
ctx.console.error('find: invalid -regex')
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
if (a === '--') {
|
|
rest.push(...argv.slice(i + 1))
|
|
break
|
|
}
|
|
if (a.startsWith('-')) {
|
|
ctx.console.error('find: unsupported ' + a)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
rest.push(a)
|
|
}
|
|
const root = rest[0] || '.'
|
|
let nameRe = null
|
|
if (nameGlob) {
|
|
const esc = nameGlob
|
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
.replace(/\*/g, '.*')
|
|
.replace(/\?/g, '.')
|
|
nameRe = new RegExp('^' + esc + '$', nameIgnoreCase ? 'i' : '')
|
|
}
|
|
let pathRe = null
|
|
if (pathGlob) {
|
|
const esc = pathGlob
|
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
.replace(/\*\*/g, '\0GLOBSTAR\0')
|
|
.replace(/\*/g, '[^/]*')
|
|
.replace(/\?/g, '[^/]')
|
|
.replace(/\0GLOBSTAR\0/g, '.*')
|
|
pathRe = new RegExp('^' + esc + '$')
|
|
}
|
|
/** @type {number | null} */
|
|
let newerThanMs = null
|
|
if (newerPath) {
|
|
try {
|
|
const st = await ctx.vfs.stat(newerPath)
|
|
if (!st) {
|
|
ctx.console.error('find: ' + newerPath + ': No such file')
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
newerThanMs = st.mtimeMs
|
|
} catch (e) {
|
|
ctx.console.error('find: ' + newerPath + ': ' + (e.message || e))
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
}
|
|
/** @type {string | null} */
|
|
let pruneAbs = null
|
|
if (prunePath) {
|
|
pruneAbs = ctx.vfs.resolveLogical(prunePath).replace(/\/+$/, '') || '/'
|
|
}
|
|
/** @type {RegExp | null} */
|
|
let regexPath = null
|
|
if (regexPathStr) {
|
|
try {
|
|
regexPath = new RegExp(regexPathStr)
|
|
} catch {
|
|
ctx.console.error('find: invalid -regex')
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
}
|
|
const execMaxRaw = ctx.vfs.env.BARE_OS_FIND_EXEC_MAX
|
|
let execMax = 64
|
|
if (execMaxRaw != null && String(execMaxRaw).trim() !== '') {
|
|
const n = Number.parseInt(String(execMaxRaw), 10)
|
|
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,
|
|
nameRe,
|
|
pathRe,
|
|
wantType,
|
|
print0,
|
|
mtimeSpec,
|
|
permSpec,
|
|
xdevRootDev,
|
|
newerThanMs,
|
|
pruneAbs,
|
|
wantEmpty,
|
|
doDelete,
|
|
regexPath,
|
|
execTemplate,
|
|
execUseOk,
|
|
execCount: 0,
|
|
execMax,
|
|
deleteState: {}
|
|
}
|
|
await walk(ctx, abs, o, 0)
|
|
}
|