449 lines
12 KiB
Plaintext
449 lines
12 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
|
|
}
|
|
|
|
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.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
|
|
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 {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 === '-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 o = {
|
|
maxDepth,
|
|
minDepth,
|
|
nameRe,
|
|
pathRe,
|
|
wantType,
|
|
print0,
|
|
mtimeSpec,
|
|
newerThanMs,
|
|
pruneAbs,
|
|
wantEmpty,
|
|
doDelete,
|
|
regexPath,
|
|
execTemplate,
|
|
execUseOk,
|
|
execCount: 0,
|
|
execMax,
|
|
deleteState: {}
|
|
}
|
|
await walk(ctx, abs, o, 0)
|
|
}
|