Files
bare-operating-system/packages/bare-os-seeder/kernel/bin/find
T
Raven Scott 346bb71ffe 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.
2026-04-04 21:23:49 -04:00

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)
}