This commit is contained in:
2026-08-18 17:51:59 -04:00
parent 783df844f1
commit d5e9798645
8 changed files with 846 additions and 637 deletions
+5 -233
View File
@@ -166,6 +166,7 @@ import { createBooterBootEmitter } from './lib/bare-os-boot-phases.js'
import { createBooterProcHelpers } from './lib/bare-os-booter-proc-helpers.js'
import { createBareOsVirtualSignalDeliverer } from './lib/bare-os-virtual-signal.js'
import { createKernelLoaderAuditAppend } from './lib/bare-os-loader-audit.js'
import { createBareOsPosixFdSimMethods } from './lib/bare-os-posix-fd-sim.js'
import { createBareOsQvacBridge } from './lib/bare-os-qvac-host.mjs'
import { bareOsQvacEnsureModelsHdms } from './lib/bare-os-qvac-models-store.mjs'
import { buildPearIpcRegistryJson } from './lib/bare-os-pear-ipc-registry.js'
@@ -317,7 +318,6 @@ import {
createReadLine
} from './lib/booter-host-session.js'
import {
wantPosixSocketFdBridge,
wantPosixFcntlBlockingWait,
bareOsPosixDgramRecvQueueMax,
bareOsPosixDgramRecvBlockMsMax,
@@ -4486,77 +4486,10 @@ async function executeKernel(disk, store, swarm, initSource) {
delete this.bareOsLogicalFdFlags[k]
}
},
bareOsPosixFdSimCapBytes() {
const raw = String(shellEnv.BARE_OS_POSIX_FD_SIM_MAX_BYTES || '').trim()
const n = raw ? Number.parseInt(raw, 10) : 1048576
return Number.isFinite(n) && n > 0
? Math.min(n, 16 * 1024 * 1024)
: 1048576
},
bareOsPosixFdSimEnabled() {
const v = shellEnv.BARE_OS_POSIX_FD_SIM
return v === '1' || v === 'true'
},
/** @returns {{ readFd: number, writeFd: number, pairId: number } | null} */
bareOsPosixFdSimPipe() {
if (!this.bareOsPosixFdSimEnabled()) return null
const st = this.bareOsPosixFdSimState
if (!st || typeof st !== 'object') return null
const pairId = st.nextPairId++
const readFd = st.nextFd++
const writeFd = st.nextFd++
if (st.nextFd > 65530) {
st.nextFd = 10
return null
}
const pk = String(pairId)
st.queues[pk] = []
st.byteTotals[pk] = 0
this.bareOsRegisterLogicalFd(readFd, `posix-pipe:${pairId}:r`)
this.bareOsRegisterLogicalFd(writeFd, `posix-pipe:${pairId}:w`)
return { readFd, writeFd, pairId }
},
/** @param {number} fd */
bareOsPosixFdSimDup(fd) {
if (!this.bareOsPosixFdSimEnabled()) return null
const k = String(Number(fd) >>> 0)
const target = this.bareOsLogicalFds[k]
if (!target || !String(target).startsWith('posix-pipe:')) return null
const st = this.bareOsPosixFdSimState
const newFd = st.nextFd++
if (st.nextFd > 65530) return null
this.bareOsRegisterLogicalFd(newFd, String(target))
return newFd
},
/**
* @param {number} writeFd
* @param {string | Uint8Array} data
*/
bareOsPosixFdSimWrite(writeFd, data) {
if (!this.bareOsPosixFdSimEnabled())
return { ok: false, reason: 'disabled' }
const k = String(Number(writeFd) >>> 0)
const t = this.bareOsLogicalFds[k]
const m = /^posix-pipe:(\d+):w$/.exec(String(t || ''))
if (!m) return { ok: false, reason: 'not_write_end' }
const pairId = m[1]
const st = this.bareOsPosixFdSimState
const q = st.queues[pairId]
if (!q) return { ok: false, reason: 'bad_pair' }
const buf =
typeof data === 'string'
? b4a.from(data, 'utf8')
: data instanceof Uint8Array
? data
: b4a.from(String(data), 'utf8')
const add = buf.byteLength
const cap = this.bareOsPosixFdSimCapBytes()
const prev = st.byteTotals[pairId] || 0
if (prev + add > cap) return { ok: false, reason: 'EAGAIN' }
st.byteTotals[pairId] = prev + add
q.push(b4a.toString(buf, 'utf8'))
return { ok: true, bytes: add }
},
...createBareOsPosixFdSimMethods({
env: shellEnv,
bootHrtimeNowNs
}),
/**
* Logical `sigaction`: `IGNORE` suppresses synthetic delivery for this signal.
* @param {string} signal
@@ -4583,167 +4516,6 @@ async function executeKernel(disk, store, swarm, initSource) {
}
throw new Error('bareOsSigaction: mode must be IGNORE or DEFAULT')
},
/**
* @param {number} readFd
* @param {number} [maxBytes]
*/
bareOsPosixFdSimRead(readFd, maxBytes = 65536) {
if (!this.bareOsPosixFdSimEnabled())
return { ok: false, reason: 'disabled' }
const cap = Math.max(1, Math.min(Number(maxBytes) || 65536, 1024 * 1024))
const k = String(Number(readFd) >>> 0)
const t = this.bareOsLogicalFds[k]
const m = /^posix-pipe:(\d+):r$/.exec(String(t || ''))
if (!m) return { ok: false, reason: 'not_read_end' }
const pairId = m[1]
const st = this.bareOsPosixFdSimState
const q = st.queues[pairId]
if (!q || q.length === 0) {
const nonblock = (this.bareOsLogicalFdFlags[k] & 0x800) === 0x800
if (nonblock) return { ok: false, reason: 'EAGAIN' }
return { ok: true, data: '', eof: false }
}
let chunk = q.shift()
if (!chunk) return { ok: true, data: '', eof: false }
if (chunk.length > cap) {
const rest = chunk.slice(cap)
chunk = chunk.slice(0, cap)
q.unshift(rest)
}
const used = b4a.from(chunk, 'utf8').byteLength
st.byteTotals[pairId] = Math.max(0, (st.byteTotals[pairId] || 0) - used)
return { ok: true, data: chunk, eof: false }
},
/**
* Non-blocking readiness probe for logical FDs (simulated pipes + stdout/stderr write).
* @param {{ fds?: Array<{ fd: number, events?: string }>, timeoutMs?: number }} [spec]
* @returns {{ ok: true, ready: Array<{ fd: number, revents: string }>, waitedMs: number, timedOut?: boolean }}
*/
bareOsPosixPollProbe(spec = {}) {
const fds = Array.isArray(spec.fds) ? spec.fds : []
/** @type {Array<{ fd: number, revents: string }>} */
const ready = []
for (const entry of fds.slice(0, 64)) {
const fd = Number(entry && entry.fd)
if (!Number.isFinite(fd) || fd < 0) continue
const ev = String(entry.events || 'rw').toLowerCase()
const wantR = ev.includes('r')
const wantW = ev.includes('w')
const k = String(fd >>> 0)
const t = this.bareOsLogicalFds[k]
const pipeR = /^posix-pipe:(\d+):r$/.exec(String(t || ''))
const pipeW = /^posix-pipe:(\d+):w$/.exec(String(t || ''))
if (wantR && pipeR) {
const st = this.bareOsPosixFdSimState
const q = st.queues[pipeR[1]]
if (q && q.length > 0) ready.push({ fd, revents: 'r' })
}
if (wantW && pipeW) {
const st = this.bareOsPosixFdSimState
const pairId = pipeW[1]
const prev = st.byteTotals[pairId] || 0
const cap = this.bareOsPosixFdSimCapBytes()
if (prev < cap) ready.push({ fd, revents: 'w' })
}
if (wantW && (k === '1' || k === '2')) {
ready.push({ fd, revents: 'w' })
}
if (wantR && wantPosixSocketFdBridge(this.env)) {
const bridge =
this.bareOsSocketBridgeByFd &&
typeof this.bareOsSocketBridgeByFd === 'object'
? this.bareOsSocketBridgeByFd[k]
: null
if (
bridge &&
(bridge.state === 'connected' || bridge.state === 'udp_bound') &&
bridge.transport === 'udp' &&
Array.isArray(bridge.dgramRecvQueue) &&
bridge.dgramRecvQueue.length > 0
) {
ready.push({ fd, revents: 'r' })
}
if (
bridge &&
bridge.state === 'connected' &&
bridge.transport === 'tcp'
) {
const tq = bridge.tcpRecvQueue
const tqlen = Array.isArray(tq) ? tq.length : 0
if (
wantR &&
(tqlen > 0 || bridge.tcpPeerEnded || bridge.tcpSockError)
) {
ready.push({ fd, revents: 'r' })
}
if (wantW && !bridge.tcpShutWr) {
ready.push({ fd, revents: 'w' })
}
}
if (
bridge &&
bridge.state === 'listening' &&
Array.isArray(bridge.acceptQueue) &&
bridge.acceptQueue.length > 0
) {
ready.push({ fd, revents: 'r' })
}
}
}
return { ok: true, ready, waitedMs: 0 }
},
/**
* Poll logical FDs with bounded wait (simulated **`poll(2)`** subset for **`BARE_OS_POSIX_FD_SIM`** pipes).
* @param {{ fds?: Array<{ fd: number, events?: string }>, timeoutMs?: number }} [spec]
*/
async bareOsPosixPoll(spec = {}) {
const maxWait = Math.min(
60000,
Math.max(0, Math.floor(Number(spec && spec.timeoutMs) || 0))
)
if (maxWait <= 0) {
const once = this.bareOsPosixPollProbe(spec)
const tProbe = bootHrtimeNowNs ? bootHrtimeNowNs() : null
return {
ok: true,
ready: once.ready,
waitedMs: 0,
timedOut: false,
pollClock: tProbe != null ? 'monotonic_hrtime' : 'Date_now'
}
}
const t0Ns = bootHrtimeNowNs ? bootHrtimeNowNs() : null
const t0Wall = Date.now()
const elapsedMs = () => {
if (t0Ns != null && bootHrtimeNowNs) {
const delta = Number(bootHrtimeNowNs() - t0Ns) / 1e6
return Math.min(60000, Math.max(0, delta))
}
return Math.min(60000, Math.max(0, Date.now() - t0Wall))
}
const interval = 10
while (elapsedMs() <= maxWait) {
const once = this.bareOsPosixPollProbe(spec)
if (once.ready.length) {
return {
ok: true,
ready: once.ready,
waitedMs: Math.round(elapsedMs()),
timedOut: false,
pollClock: t0Ns != null ? 'monotonic_hrtime' : 'Date_now'
}
}
if (elapsedMs() >= maxWait) break
await new Promise((res) => setTimeout(res, interval))
}
return {
ok: true,
ready: [],
waitedMs: Math.round(Math.min(maxWait, elapsedMs())),
timedOut: true,
pollClock: t0Ns != null ? 'monotonic_hrtime' : 'Date_now'
}
},
/** @type {Record<string, unknown>[]} */
bareOsSnapshotHandles: [],
/** @param {Record<string, unknown>} desc */
@@ -0,0 +1,250 @@
/**
* Simulated POSIX pipe FDs and poll probe methods attached to the kernel `ctx`.
*/
import b4a from 'b4a'
import { wantPosixSocketFdBridge } from './bare-os-posix-syscall-helpers.js'
/**
* @param {{
* env: Record<string, string | undefined>,
* bootHrtimeNowNs: (() => bigint) | null
* }} deps
*/
export function createBareOsPosixFdSimMethods(deps) {
const { env, bootHrtimeNowNs } = deps
return {
bareOsPosixFdSimCapBytes() {
const raw = String(env.BARE_OS_POSIX_FD_SIM_MAX_BYTES || '').trim()
const n = raw ? Number.parseInt(raw, 10) : 1048576
return Number.isFinite(n) && n > 0
? Math.min(n, 16 * 1024 * 1024)
: 1048576
},
bareOsPosixFdSimEnabled() {
const v = env.BARE_OS_POSIX_FD_SIM
return v === '1' || v === 'true'
},
/** @returns {{ readFd: number, writeFd: number, pairId: number } | null} */
bareOsPosixFdSimPipe() {
if (!this.bareOsPosixFdSimEnabled()) return null
const st = this.bareOsPosixFdSimState
if (!st || typeof st !== 'object') return null
const pairId = st.nextPairId++
const readFd = st.nextFd++
const writeFd = st.nextFd++
if (st.nextFd > 65530) {
st.nextFd = 10
return null
}
const pk = String(pairId)
st.queues[pk] = []
st.byteTotals[pk] = 0
this.bareOsRegisterLogicalFd(readFd, `posix-pipe:${pairId}:r`)
this.bareOsRegisterLogicalFd(writeFd, `posix-pipe:${pairId}:w`)
return { readFd, writeFd, pairId }
},
/** @param {number} fd */
bareOsPosixFdSimDup(fd) {
if (!this.bareOsPosixFdSimEnabled()) return null
const k = String(Number(fd) >>> 0)
const target = this.bareOsLogicalFds[k]
if (!target || !String(target).startsWith('posix-pipe:')) return null
const st = this.bareOsPosixFdSimState
const newFd = st.nextFd++
if (st.nextFd > 65530) return null
this.bareOsRegisterLogicalFd(newFd, String(target))
return newFd
},
/**
* @param {number} writeFd
* @param {string | Uint8Array} data
*/
bareOsPosixFdSimWrite(writeFd, data) {
if (!this.bareOsPosixFdSimEnabled())
return { ok: false, reason: 'disabled' }
const k = String(Number(writeFd) >>> 0)
const t = this.bareOsLogicalFds[k]
const m = /^posix-pipe:(\d+):w$/.exec(String(t || ''))
if (!m) return { ok: false, reason: 'not_write_end' }
const pairId = m[1]
const st = this.bareOsPosixFdSimState
const q = st.queues[pairId]
if (!q) return { ok: false, reason: 'bad_pair' }
const buf =
typeof data === 'string'
? b4a.from(data, 'utf8')
: data instanceof Uint8Array
? data
: b4a.from(String(data), 'utf8')
const add = buf.byteLength
const cap = this.bareOsPosixFdSimCapBytes()
const prev = st.byteTotals[pairId] || 0
if (prev + add > cap) return { ok: false, reason: 'EAGAIN' }
st.byteTotals[pairId] = prev + add
q.push(b4a.toString(buf, 'utf8'))
return { ok: true, bytes: add }
},
/**
* @param {number} readFd
* @param {number} [maxBytes]
*/
bareOsPosixFdSimRead(readFd, maxBytes = 65536) {
if (!this.bareOsPosixFdSimEnabled())
return { ok: false, reason: 'disabled' }
const cap = Math.max(1, Math.min(Number(maxBytes) || 65536, 1024 * 1024))
const k = String(Number(readFd) >>> 0)
const t = this.bareOsLogicalFds[k]
const m = /^posix-pipe:(\d+):r$/.exec(String(t || ''))
if (!m) return { ok: false, reason: 'not_read_end' }
const pairId = m[1]
const st = this.bareOsPosixFdSimState
const q = st.queues[pairId]
if (!q || q.length === 0) {
const nonblock = (this.bareOsLogicalFdFlags[k] & 0x800) === 0x800
if (nonblock) return { ok: false, reason: 'EAGAIN' }
return { ok: true, data: '', eof: false }
}
let chunk = q.shift()
if (!chunk) return { ok: true, data: '', eof: false }
if (chunk.length > cap) {
const rest = chunk.slice(cap)
chunk = chunk.slice(0, cap)
q.unshift(rest)
}
const used = b4a.from(chunk, 'utf8').byteLength
st.byteTotals[pairId] = Math.max(0, (st.byteTotals[pairId] || 0) - used)
return { ok: true, data: chunk, eof: false }
},
/**
* Non-blocking readiness probe for logical FDs (simulated pipes + stdout/stderr write).
* @param {{ fds?: Array<{ fd: number, events?: string }>, timeoutMs?: number }} [spec]
* @returns {{ ok: true, ready: Array<{ fd: number, revents: string }>, waitedMs: number, timedOut?: boolean }}
*/
bareOsPosixPollProbe(spec = {}) {
const fds = Array.isArray(spec.fds) ? spec.fds : []
/** @type {Array<{ fd: number, revents: string }>} */
const ready = []
for (const entry of fds.slice(0, 64)) {
const fd = Number(entry && entry.fd)
if (!Number.isFinite(fd) || fd < 0) continue
const ev = String(entry.events || 'rw').toLowerCase()
const wantR = ev.includes('r')
const wantW = ev.includes('w')
const k = String(fd >>> 0)
const t = this.bareOsLogicalFds[k]
const pipeR = /^posix-pipe:(\d+):r$/.exec(String(t || ''))
const pipeW = /^posix-pipe:(\d+):w$/.exec(String(t || ''))
if (wantR && pipeR) {
const st = this.bareOsPosixFdSimState
const q = st.queues[pipeR[1]]
if (q && q.length > 0) ready.push({ fd, revents: 'r' })
}
if (wantW && pipeW) {
const st = this.bareOsPosixFdSimState
const pairId = pipeW[1]
const prev = st.byteTotals[pairId] || 0
const cap = this.bareOsPosixFdSimCapBytes()
if (prev < cap) ready.push({ fd, revents: 'w' })
}
if (wantW && (k === '1' || k === '2')) {
ready.push({ fd, revents: 'w' })
}
if (wantR && wantPosixSocketFdBridge(this.env)) {
const bridge =
this.bareOsSocketBridgeByFd &&
typeof this.bareOsSocketBridgeByFd === 'object'
? this.bareOsSocketBridgeByFd[k]
: null
if (
bridge &&
(bridge.state === 'connected' || bridge.state === 'udp_bound') &&
bridge.transport === 'udp' &&
Array.isArray(bridge.dgramRecvQueue) &&
bridge.dgramRecvQueue.length > 0
) {
ready.push({ fd, revents: 'r' })
}
if (
bridge &&
bridge.state === 'connected' &&
bridge.transport === 'tcp'
) {
const tq = bridge.tcpRecvQueue
const tqlen = Array.isArray(tq) ? tq.length : 0
if (
wantR &&
(tqlen > 0 || bridge.tcpPeerEnded || bridge.tcpSockError)
) {
ready.push({ fd, revents: 'r' })
}
if (wantW && !bridge.tcpShutWr) {
ready.push({ fd, revents: 'w' })
}
}
if (
bridge &&
bridge.state === 'listening' &&
Array.isArray(bridge.acceptQueue) &&
bridge.acceptQueue.length > 0
) {
ready.push({ fd, revents: 'r' })
}
}
}
return { ok: true, ready, waitedMs: 0 }
},
/**
* Poll logical FDs with bounded wait (simulated **`poll(2)`** subset for **`BARE_OS_POSIX_FD_SIM`** pipes).
* @param {{ fds?: Array<{ fd: number, events?: string }>, timeoutMs?: number }} [spec]
*/
async bareOsPosixPoll(spec = {}) {
const maxWait = Math.min(
60000,
Math.max(0, Math.floor(Number(spec && spec.timeoutMs) || 0))
)
if (maxWait <= 0) {
const once = this.bareOsPosixPollProbe(spec)
const tProbe = bootHrtimeNowNs ? bootHrtimeNowNs() : null
return {
ok: true,
ready: once.ready,
waitedMs: 0,
timedOut: false,
pollClock: tProbe != null ? 'monotonic_hrtime' : 'Date_now'
}
}
const t0Ns = bootHrtimeNowNs ? bootHrtimeNowNs() : null
const t0Wall = Date.now()
const elapsedMs = () => {
if (t0Ns != null && bootHrtimeNowNs) {
const delta = Number(bootHrtimeNowNs() - t0Ns) / 1e6
return Math.min(60000, Math.max(0, delta))
}
return Math.min(60000, Math.max(0, Date.now() - t0Wall))
}
const interval = 10
while (elapsedMs() <= maxWait) {
const once = this.bareOsPosixPollProbe(spec)
if (once.ready.length) {
return {
ok: true,
ready: once.ready,
waitedMs: Math.round(elapsedMs()),
timedOut: false,
pollClock: t0Ns != null ? 'monotonic_hrtime' : 'Date_now'
}
}
if (elapsedMs() >= maxWait) break
await new Promise((res) => setTimeout(res, interval))
}
return {
ok: true,
ready: [],
waitedMs: Math.round(Math.min(maxWait, elapsedMs())),
timedOut: true,
pollClock: t0Ns != null ? 'monotonic_hrtime' : 'Date_now'
}
}
}
}
@@ -0,0 +1,64 @@
/**
* Pipeline child-ctx clone, raw-chunk capture, and optional stage timeout.
*/
/**
* Shallow clone for pipeline **`/bin`** execution: session **`env`**, optional **`shellStdin`**, and
* **`bareOsStdoutCaptured`** when stdout is captured (pipe to next stage or **`>`** redirect) so
* utilities (e.g. **`ls`**) can use one-record-per-line output.
* @param {Record<string, unknown>} ctx
* @param {Record<string, string>} env
* @param {string | null} stdinText
* @param {boolean} bareOsStdoutCaptured
*/
export function bareOsPipelineChildCtx(ctx, env, stdinText, bareOsStdoutCaptured) {
const o = Object.assign({}, ctx, {
env: env && typeof env === 'object' ? { ...env } : env,
bareOsStdoutCaptured
})
if (stdinText != null) o.shellStdin = stdinText
return o
}
/**
* Convert raw binary chunks into shell pipeline capture text.
* Pipeline transport is text-based, so bytes are mapped 1:1 via char codes.
* @param {string | Uint8Array} chunk
*/
export function bareOsPipelineRawChunkToText(chunk) {
if (typeof chunk === 'string') return chunk
if (!(chunk instanceof Uint8Array)) return String(chunk)
let s = ''
for (let i = 0; i < chunk.length; i++) s += String.fromCharCode(chunk[i])
return s
}
/**
* Optional timeout guard for potentially stalled pipeline stages.
* @param {Record<string, string>} env
* @param {() => Promise<unknown>} run
* @param {string} label
*/
export async function runWithShellPipelineStageTimeout(env, run, label) {
const raw = Number.parseInt(String(env.BARE_OS_SHELL_PIPELINE_STAGE_TIMEOUT_MS || ''), 10)
const timeoutMs = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 120000) : 0
if (!timeoutMs) return await run()
/** @type {ReturnType<typeof setTimeout> | null} */
let timer = null
try {
return await Promise.race([
run(),
new Promise((_, reject) => {
timer = setTimeout(() => {
reject(
new Error(
`shell: pipeline stage timeout (${timeoutMs}ms): ${label}`
)
)
}, timeoutMs)
})
])
} finally {
if (timer) clearTimeout(timer)
}
}
+181
View File
@@ -0,0 +1,181 @@
/**
* Small statement helpers: local/declare, limited `[[ ]]`, reserved-word
* diagnostics, function positional env, and `case` pattern lists.
*/
import { expandWord } from './shell-expand.js'
/**
* @typedef {{ type: string, value: string }} ShellStmtToken
*/
/**
* @param {Record<string, unknown>} ctx
* @param {ShellStmtToken[]} rest
* @returns {Promise<'exit' | 'ok'>}
*/
export async function execShellLocalBuiltin(ctx, rest) {
const vfs = ctx.vfs
const env = vfs?.env
if (!env || typeof env !== 'object') {
ctx.exitCode = 0
return 'ok'
}
let n = 0
for (const t of rest) {
if (++n > 48) break
if (t.type !== 'word') continue
const eq = t.value.indexOf('=')
if (eq <= 0) continue
const name = t.value.slice(0, eq).trim()
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
env[name] = expandWord(t.value.slice(eq + 1), env)
}
ctx.exitCode = 0
return 'ok'
}
/**
* @param {Record<string, unknown>} ctx
* @param {ShellStmtToken[]} rest
* @returns {'ok' | null}
*/
export function tryExecShellDeclareBuiltin(ctx, rest) {
const vfs = ctx.vfs
const env = vfs?.env
if (!env || typeof env !== 'object') return 'ok'
if (rest[0]?.type !== 'word' || rest[0].value !== '-r') return null
let n = 0
for (let i = 1; i < rest.length; i++) {
if (++n > 32) break
const t = rest[i]
if (t.type !== 'word') continue
const eq = t.value.indexOf('=')
if (eq <= 0) continue
const name = t.value.slice(0, eq).trim()
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
env[name] = expandWord(t.value.slice(eq + 1), env)
}
ctx.exitCode = 0
return 'ok'
}
/** Reserved words that cannot begin a simple or compound statement (POSIX-style). */
export const BARE_OS_SHELL_MISPLACED_STATEMENT_START = new Set([
'then',
'else',
'elif',
'fi',
'do',
'done',
'esac',
'in'
])
/**
* @param {Record<string, unknown>} ctx
* @param {ShellStmtToken[]} stmt
* @returns {boolean} true when an error was reported (caller should return)
*/
export function tryReportMisplacedReservedStatementStart(ctx, stmt) {
const h = stmt[0]
if (!h || h.type !== 'word') return false
const w = h.value
if (!BARE_OS_SHELL_MISPLACED_STATEMENT_START.has(w)) return false
ctx.console.error(
`shell: syntax error: reserved word '${w}' cannot start a statement`
)
ctx.exitCode = 2
return true
}
/**
* Minimal gated **`[[ … ]]`** — only **`[[ WORD == WORD ]]`** and **`[[ WORD != WORD ]]`**.
* @param {Record<string, unknown>} ctx
* @param {ShellStmtToken[]} stmt
* @returns {Promise<'exit' | 'ok'>}
*/
export async function execDoubleBracketLimited(ctx, stmt) {
const vfs = ctx.vfs
const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {}
const last = stmt[stmt.length - 1]
if (!last || last.type !== 'word' || last.value !== ']]') {
ctx.console.error('shell: [[: missing closing ]]')
ctx.exitCode = 2
return 'ok'
}
const bodyStart = stmt[0]?.value === '[[' ? 1 : 2
const inner = stmt.slice(bodyStart, -1)
if (
inner.length === 3 &&
inner[0].type === 'word' &&
inner[1].type === 'word' &&
inner[2].type === 'word'
) {
const op = inner[1].value
if (op === '==' || op === '!=') {
const a = expandWord(inner[0].value, env)
const b = expandWord(inner[2].value, env)
ctx.exitCode = op === '==' ? (a === b ? 0 : 1) : a !== b ? 0 : 1
return 'ok'
}
}
ctx.console.error(
'shell: [[: only `[[ WORD == WORD ]]` and `[[ WORD != WORD ]]` are supported'
)
ctx.exitCode = 2
return 'ok'
}
/**
* @param {Record<string, string>} env
* @param {string[]} argv
*/
export function assignShellFunctionPositionalEnv(env, argv) {
env['0'] = argv[0] || ''
const fnArgs = argv.slice(1)
for (let i = 1; i <= 9; i++) env[String(i)] = fnArgs[i - 1] ?? ''
env['#'] = String(fnArgs.length)
}
/**
* @param {ShellStmtToken[]} toks
* @param {Record<string, string>} env
* @returns {string[]}
*/
export function casePatternList(toks, env) {
/** @type {string[]} */
const out = []
/** @type {ShellStmtToken[]} */
let cur = []
for (const t of toks) {
if (t.type === 'op' && t.value === '|') {
if (cur.length) {
const s = cur.map((w) => w.value).join(' ')
out.push(expandWord(s.trim(), env))
cur = []
}
} else if (t.type === 'word') {
cur.push(t)
}
}
if (cur.length) {
const s = cur.map((w) => w.value).join(' ')
out.push(expandWord(s.trim(), env))
}
return out.filter(Boolean)
}
/**
* Clear simulated background jobs on guest ↔ unlocked transitions (POSIX session model).
* @param {Record<string, unknown>} ctx
*/
export function bareOsResetShellIdentityState(ctx) {
if (
ctx.shellBackgroundJobs &&
typeof ctx.shellBackgroundJobs === 'object' &&
Array.isArray(ctx.shellBackgroundJobs.list)
) {
ctx.shellBackgroundJobs.list.length = 0
ctx.shellBackgroundJobs.nextId = 1
}
}
+14 -237
View File
@@ -54,6 +54,19 @@ import {
mergePipelineChildCtx
} from './shell-runtime.js'
import { expandWord, expandShellWordTokens } from './shell-expand.js'
import {
bareOsPipelineChildCtx,
bareOsPipelineRawChunkToText,
runWithShellPipelineStageTimeout
} from './shell-pipeline.js'
import {
execShellLocalBuiltin,
tryExecShellDeclareBuiltin,
tryReportMisplacedReservedStatementStart,
execDoubleBracketLimited,
assignShellFunctionPositionalEnv,
casePatternList
} from './shell-stmt.js'
export { BARE_OS_SHELL_NOUNSET_ERROR }
export {
@@ -87,6 +100,7 @@ export {
planShellRedirections,
buildShellExecutionGraph
} from './shell-parse.js'
export { bareOsResetShellIdentityState } from './shell-stmt.js'
/** Shell planner/executor continues below. */
@@ -120,67 +134,6 @@ export async function dispatchShellTrapSignal(ctx, signal) {
return true
}
/**
* Shallow clone for pipeline **`/bin`** execution: session **`env`**, optional **`shellStdin`**, and
* **`bareOsStdoutCaptured`** when stdout is captured (pipe to next stage or **`>`** redirect) so
* utilities (e.g. **`ls`**) can use one-record-per-line output.
* @param {Record<string, unknown>} ctx
* @param {Record<string, string>} env
* @param {string | null} stdinText
* @param {boolean} bareOsStdoutCaptured
*/
function bareOsPipelineChildCtx(ctx, env, stdinText, bareOsStdoutCaptured) {
const o = Object.assign({}, ctx, {
env: env && typeof env === 'object' ? { ...env } : env,
bareOsStdoutCaptured
})
if (stdinText != null) o.shellStdin = stdinText
return o
}
/**
* Convert raw binary chunks into shell pipeline capture text.
* Pipeline transport is text-based, so bytes are mapped 1:1 via char codes.
* @param {string | Uint8Array} chunk
*/
function bareOsPipelineRawChunkToText(chunk) {
if (typeof chunk === 'string') return chunk
if (!(chunk instanceof Uint8Array)) return String(chunk)
let s = ''
for (let i = 0; i < chunk.length; i++) s += String.fromCharCode(chunk[i])
return s
}
/**
* Optional timeout guard for potentially stalled pipeline stages.
* @param {Record<string, string>} env
* @param {() => Promise<unknown>} run
* @param {string} label
*/
async function runWithShellPipelineStageTimeout(env, run, label) {
const raw = Number.parseInt(String(env.BARE_OS_SHELL_PIPELINE_STAGE_TIMEOUT_MS || ''), 10)
const timeoutMs = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 120000) : 0
if (!timeoutMs) return await run()
/** @type {ReturnType<typeof setTimeout> | null} */
let timer = null
try {
return await Promise.race([
run(),
new Promise((_, reject) => {
timer = setTimeout(() => {
reject(
new Error(
`shell: pipeline stage timeout (${timeoutMs}ms): ${label}`
)
)
}, timeoutMs)
})
])
} finally {
if (timer) clearTimeout(timer)
}
}
/**
* @param {Record<string, unknown>} ctx
* @param {SimpleCmd[]} pipeline
@@ -1569,34 +1522,6 @@ async function execForConstruct(ctx, tokens) {
return 'ok'
}
/**
* @param {Token[]} toks
* @param {Record<string, string>} env
* @returns {string[]}
*/
function casePatternList(toks, env) {
/** @type {string[]} */
const out = []
/** @type {Token[]} */
let cur = []
for (const t of toks) {
if (t.type === 'op' && t.value === '|') {
if (cur.length) {
const s = cur.map((w) => w.value).join(' ')
out.push(expandWord(s.trim(), env))
cur = []
}
} else if (t.type === 'word') {
cur.push(t)
}
}
if (cur.length) {
const s = cur.map((w) => w.value).join(' ')
out.push(expandWord(s.trim(), env))
}
return out.filter(Boolean)
}
/**
* `case WORD in pattern) list ;; … esac` — bounded branches; patterns support `|` alternation and `*`.
* @param {Record<string, unknown>} ctx
@@ -1692,128 +1617,6 @@ async function execCaseConstruct(ctx, tokens) {
return 'ok'
}
/**
* @param {Record<string, unknown>} ctx
* @param {Token[]} stmt
* @returns {Promise<'exit' | 'ok'>}
*/
/**
* @param {Record<string, unknown>} ctx
* @param {{ type: string, value: string }[]} rest
*/
async function execShellLocalBuiltin(ctx, rest) {
const vfs = ctx.vfs
const env = vfs?.env
if (!env || typeof env !== 'object') {
ctx.exitCode = 0
return 'ok'
}
let n = 0
for (const t of rest) {
if (++n > 48) break
if (t.type !== 'word') continue
const eq = t.value.indexOf('=')
if (eq <= 0) continue
const name = t.value.slice(0, eq).trim()
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
env[name] = expandWord(t.value.slice(eq + 1), env)
}
ctx.exitCode = 0
return 'ok'
}
/**
* @param {Record<string, unknown>} ctx
* @param {{ type: string, value: string }[]} rest
* @returns {'ok' | null}
*/
function tryExecShellDeclareBuiltin(ctx, rest) {
const vfs = ctx.vfs
const env = vfs?.env
if (!env || typeof env !== 'object') return 'ok'
if (rest[0]?.type !== 'word' || rest[0].value !== '-r') return null
let n = 0
for (let i = 1; i < rest.length; i++) {
if (++n > 32) break
const t = rest[i]
if (t.type !== 'word') continue
const eq = t.value.indexOf('=')
if (eq <= 0) continue
const name = t.value.slice(0, eq).trim()
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
env[name] = expandWord(t.value.slice(eq + 1), env)
}
ctx.exitCode = 0
return 'ok'
}
/** Reserved words that cannot begin a simple or compound statement (POSIX-style). */
const BARE_OS_SHELL_MISPLACED_STATEMENT_START = new Set([
'then',
'else',
'elif',
'fi',
'do',
'done',
'esac',
'in'
])
/**
* @param {Record<string, unknown>} ctx
* @param {Token[]} stmt
* @returns {boolean} true when an error was reported (caller should return)
*/
function tryReportMisplacedReservedStatementStart(ctx, stmt) {
const h = stmt[0]
if (!h || h.type !== 'word') return false
const w = h.value
if (!BARE_OS_SHELL_MISPLACED_STATEMENT_START.has(w)) return false
ctx.console.error(
`shell: syntax error: reserved word '${w}' cannot start a statement`
)
ctx.exitCode = 2
return true
}
/**
* Minimal gated **`[[ … ]]`** — only **`[[ WORD == WORD ]]`** and **`[[ WORD != WORD ]]`**.
* @param {Record<string, unknown>} ctx
* @param {Token[]} stmt
* @returns {Promise<'exit' | 'ok'>}
*/
async function execDoubleBracketLimited(ctx, stmt) {
const vfs = ctx.vfs
const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {}
const last = stmt[stmt.length - 1]
if (!last || last.type !== 'word' || last.value !== ']]') {
ctx.console.error('shell: [[: missing closing ]]')
ctx.exitCode = 2
return 'ok'
}
const bodyStart = stmt[0]?.value === '[[' ? 1 : 2
const inner = stmt.slice(bodyStart, -1)
if (
inner.length === 3 &&
inner[0].type === 'word' &&
inner[1].type === 'word' &&
inner[2].type === 'word'
) {
const op = inner[1].value
if (op === '==' || op === '!=') {
const a = expandWord(inner[0].value, env)
const b = expandWord(inner[2].value, env)
ctx.exitCode = op === '==' ? (a === b ? 0 : 1) : a !== b ? 0 : 1
return 'ok'
}
}
ctx.console.error(
'shell: [[: only `[[ WORD == WORD ]]` and `[[ WORD != WORD ]]` are supported'
)
ctx.exitCode = 2
return 'ok'
}
async function dispatchShellStatement(ctx, stmt) {
/** Normalize inline brace-expression tokens like `{1..5}` into a word token so
* they use normal shell word expansion instead of statement/group operators. */
@@ -2091,17 +1894,6 @@ async function execAndOrList(ctx, tokens) {
return 'ok'
}
/**
* @param {Record<string, string>} env
* @param {string[]} argv
*/
function assignShellFunctionPositionalEnv(env, argv) {
env['0'] = argv[0] || ''
const fnArgs = argv.slice(1)
for (let i = 1; i <= 9; i++) env[String(i)] = fnArgs[i - 1] ?? ''
env['#'] = String(fnArgs.length)
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} line
@@ -2292,18 +2084,3 @@ async function execShellLineInner(ctx, rawTrimmed) {
syncBareOsExitStatusEnv(ctx)
return 'ok'
}
/**
* Clear simulated background jobs on guest ↔ unlocked transitions (POSIX session model).
* @param {Record<string, unknown>} ctx
*/
export function bareOsResetShellIdentityState(ctx) {
if (
ctx.shellBackgroundJobs &&
typeof ctx.shellBackgroundJobs === 'object' &&
Array.isArray(ctx.shellBackgroundJobs.list)
) {
ctx.shellBackgroundJobs.list.length = 0
ctx.shellBackgroundJobs.nextId = 1
}
}
+205
View File
@@ -0,0 +1,205 @@
/**
* VFS mount / mirror / tilde / system-RO alias routers used by createVfs.
*/
import unixPathResolve from 'unix-path-resolve'
/**
* @param {{
* getCwd: () => string,
* normalizeHome: () => string,
* mntRef?: { getMounts?: () => Map<string, { drive: unknown, writable: boolean }> } | null,
* systemRoAliasNorm: string,
* getAuxiliaryMountLines?: (() => (string | null | undefined)[]) | null,
* getAuxiliaryDrives?: (() => unknown[] | null | undefined) | null,
* systemDrive: unknown
* }} deps
*/
export function createVfsRouteHelpers(deps) {
const {
getCwd,
normalizeHome,
mntRef = null,
systemRoAliasNorm,
getAuxiliaryMountLines = null,
getAuxiliaryDrives = null,
systemDrive
} = deps
/**
* `cd ~`, `touch ~/x`, etc. must map to $HOME — otherwise `unix-path-resolve(cwd, '~')`
* becomes `/~` on the system drive (read-only).
*/
function expandTilde(userPath) {
const h = normalizeHome()
if (userPath === '~') return h
if (userPath.startsWith('~/')) {
const rest = userPath.slice(2)
return rest ? unixPathResolve(h, rest) : h
}
return userPath
}
/** Logical absolute path from cwd + user path */
function resolveLogical(userPath) {
const expanded = expandTilde(userPath)
return unixPathResolve(getCwd(), expanded)
}
function getMntMap() {
if (mntRef && typeof mntRef.getMounts === 'function')
return mntRef.getMounts()
return new Map()
}
/** Linux-shaped mount table: root + HDMS `/mnt/<label>` rows (best-effort). */
function pseudoMountsText() {
const lines = ['bare-os-root / hyperdrive ro 0 0']
if (systemRoAliasNorm)
lines.push(
`bare-os-system-ro-alias ${systemRoAliasNorm} hyperdrive ro 0 0`
)
const map = getMntMap()
const keys = [...map.keys()].sort((a, b) => a.localeCompare(b))
for (const label of keys) {
const ent = map.get(label)
if (!ent) continue
const rw = ent.writable ? 'rw' : 'ro'
lines.push(`bare-os-${label} /mnt/${label} hyperdrive ${rw} 0 0`)
}
try {
const extra = getAuxiliaryMountLines ? getAuxiliaryMountLines() : []
if (Array.isArray(extra)) {
for (const row of extra) {
if (typeof row === 'string' && row.trim()) lines.push(row.trim())
}
}
} catch {
/* ignore */
}
return lines.join('\n') + '\n'
}
/**
* Optional read-only auxiliary Hyperdrives under `/mirror/aux0`, `/mirror/aux1`, …
* @returns {null | Record<string, unknown>}
*/
function routeMirror(absPath) {
const aux = getAuxiliaryDrives ? getAuxiliaryDrives() : null
if (!Array.isArray(aux) || aux.length === 0) return null
if (absPath === '/mirror' || absPath === '/mirror/') {
return { virtualMirrorRoot: true }
}
if (!absPath.startsWith('/mirror/')) return null
const rest = absPath.slice('/mirror/'.length)
const slash = rest.indexOf('/')
const seg = slash === -1 ? rest : rest.slice(0, slash)
const tail = slash === -1 ? '' : rest.slice(slash + 1)
if (!seg) return { virtualMirrorRoot: true }
const m = /^aux(\d+)$/.exec(seg)
if (!m) {
return { drive: systemDrive, path: absPath }
}
const idx = Number(m[1])
const drive = /** @type {unknown} */ (aux[idx])
if (
!drive ||
typeof (/** @type {{ get?: unknown }} */ (drive).get) !== 'function'
) {
return { drive: systemDrive, path: absPath }
}
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
const p = unixPathResolve('/', sub)
return {
drive: /** @type {import('hyperdrive').default} */ (drive),
path: p,
mntReadOnly: true
}
}
/**
* @returns {null | Record<string, unknown>}
*/
function routeMnt(absPath) {
if (absPath === '/mnt' || absPath === '/mnt/') {
return { virtualMntRoot: true }
}
if (!absPath.startsWith('/mnt/')) return null
const rest = absPath.slice(5)
const slash = rest.indexOf('/')
const label = slash === -1 ? rest : rest.slice(0, slash)
const tail = slash === -1 ? '' : rest.slice(slash + 1)
if (!label) return { virtualMntRoot: true }
const mounts = getMntMap()
const ent = mounts.get(label)
if (!ent) {
return { drive: systemDrive, path: absPath }
}
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
const p = unixPathResolve('/', sub)
return { drive: ent.drive, path: p, mntReadOnly: !ent.writable }
}
/**
* Map logical absolute path to { drive, path } for Hyperdrive ops.
* Virtual /home lists only the active session dir; /home/<active>/… is the personal drive.
*/
function routeSystemRoAlias(absPath) {
if (!systemRoAliasNorm || !systemRoAliasNorm.startsWith('/')) return null
const norm = absPath.replace(/\/+$/, '') || '/'
if (
norm !== systemRoAliasNorm &&
!absPath.startsWith(systemRoAliasNorm + '/')
)
return null
let tail = '/'
if (norm !== systemRoAliasNorm) {
tail = absPath.slice(systemRoAliasNorm.length) || '/'
}
const p = tail.startsWith('/') ? tail : '/' + tail
const resolved = unixPathResolve('/', p.replace(/^\/+/, '') || '/')
return {
drive: systemDrive,
path: resolved,
mntReadOnly: true
}
}
/**
* When HDMS (or other) mounts label `www` at `/mnt/www`, expose it at `$HOME/.www`.
* @param {string} relFromHomeRoot path under the session home root (e.g. `.www`, `.www/a`)
* @returns {{ drive: unknown, path: string, mntReadOnly: boolean } | null}
*/
function routeHomeWwwAlias(relFromHomeRoot) {
const rel = String(relFromHomeRoot || '').replace(/^\/+/, '')
if (rel !== '.www' && !rel.startsWith('.www/')) return null
const mounts = getMntMap()
const ent = mounts.get('www')
if (!ent) return null
const wwwPrefix = '.www/'
const tail =
rel === '.www'
? ''
: rel.startsWith(wwwPrefix)
? rel.slice(wwwPrefix.length)
: null
if (tail === null && rel !== '.www') return null
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
const p = unixPathResolve('/', sub)
return {
drive: ent.drive,
path: p,
mntReadOnly: !ent.writable
}
}
return {
expandTilde,
resolveLogical,
getMntMap,
pseudoMountsText,
routeMirror,
routeMnt,
routeSystemRoAlias,
routeHomeWwwAlias
}
}
+19 -167
View File
@@ -32,6 +32,7 @@ import {
assertNotBootPolicyDenyVfs as assertNotBootPolicyDenyVfsPolicy
} from './vfs-policy.js'
import { createVfsWarmReadCache } from './vfs-warm-cache.js'
import { createVfsRouteHelpers } from './vfs-route.js'
import {
BARE_OS_PROC_FILE_TO_ID_REPLICATION_OPERATOR_SURFACE,
BARE_OS_PROC_FILE_TO_ID_PEAR_CORESTORE_HRPC,
@@ -625,6 +626,24 @@ export function createVfs(
})
let cwd = env.PWD || HOME()
const {
resolveLogical,
getMntMap,
pseudoMountsText,
routeMirror,
routeMnt,
routeSystemRoAlias,
routeHomeWwwAlias
} = createVfsRouteHelpers({
getCwd: () => cwd,
normalizeHome,
mntRef,
systemRoAliasNorm,
getAuxiliaryMountLines,
getAuxiliaryDrives,
systemDrive
})
/**
* @param {unknown} drive
* @param {string} personalPath
@@ -1773,173 +1792,6 @@ export function createVfs(
return st
}
/**
* `cd ~`, `touch ~/x`, etc. must map to $HOME — otherwise `unix-path-resolve(cwd, '~')`
* becomes `/~` on the system drive (read-only).
*/
function expandTilde(userPath) {
const h = normalizeHome()
if (userPath === '~') return h
if (userPath.startsWith('~/')) {
const rest = userPath.slice(2)
return rest ? unixPathResolve(h, rest) : h
}
return userPath
}
/** Logical absolute path from cwd + user path */
function resolveLogical(userPath) {
const expanded = expandTilde(userPath)
return unixPathResolve(cwd, expanded)
}
function getMntMap() {
if (mntRef && typeof mntRef.getMounts === 'function')
return mntRef.getMounts()
return new Map()
}
/** Linux-shaped mount table: root + HDMS `/mnt/<label>` rows (best-effort). */
function pseudoMountsText() {
const lines = ['bare-os-root / hyperdrive ro 0 0']
if (systemRoAliasNorm)
lines.push(
`bare-os-system-ro-alias ${systemRoAliasNorm} hyperdrive ro 0 0`
)
const map = getMntMap()
const keys = [...map.keys()].sort((a, b) => a.localeCompare(b))
for (const label of keys) {
const ent = map.get(label)
if (!ent) continue
const rw = ent.writable ? 'rw' : 'ro'
lines.push(`bare-os-${label} /mnt/${label} hyperdrive ${rw} 0 0`)
}
try {
const extra = getAuxiliaryMountLines ? getAuxiliaryMountLines() : []
if (Array.isArray(extra)) {
for (const row of extra) {
if (typeof row === 'string' && row.trim()) lines.push(row.trim())
}
}
} catch {
/* ignore */
}
return lines.join('\n') + '\n'
}
/**
* Optional read-only auxiliary Hyperdrives under `/mirror/aux0`, `/mirror/aux1`, …
* @returns {null | Record<string, unknown>}
*/
function routeMirror(absPath) {
const aux = getAuxiliaryDrives ? getAuxiliaryDrives() : null
if (!Array.isArray(aux) || aux.length === 0) return null
if (absPath === '/mirror' || absPath === '/mirror/') {
return { virtualMirrorRoot: true }
}
if (!absPath.startsWith('/mirror/')) return null
const rest = absPath.slice('/mirror/'.length)
const slash = rest.indexOf('/')
const seg = slash === -1 ? rest : rest.slice(0, slash)
const tail = slash === -1 ? '' : rest.slice(slash + 1)
if (!seg) return { virtualMirrorRoot: true }
const m = /^aux(\d+)$/.exec(seg)
if (!m) {
return { drive: systemDrive, path: absPath }
}
const idx = Number(m[1])
const drive = /** @type {unknown} */ (aux[idx])
if (
!drive ||
typeof (/** @type {{ get?: unknown }} */ (drive).get) !== 'function'
) {
return { drive: systemDrive, path: absPath }
}
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
const p = unixPathResolve('/', sub)
return {
drive: /** @type {import('hyperdrive').default} */ (drive),
path: p,
mntReadOnly: true
}
}
/**
* @returns {null | Record<string, unknown>}
*/
function routeMnt(absPath) {
if (absPath === '/mnt' || absPath === '/mnt/') {
return { virtualMntRoot: true }
}
if (!absPath.startsWith('/mnt/')) return null
const rest = absPath.slice(5)
const slash = rest.indexOf('/')
const label = slash === -1 ? rest : rest.slice(0, slash)
const tail = slash === -1 ? '' : rest.slice(slash + 1)
if (!label) return { virtualMntRoot: true }
const mounts = getMntMap()
const ent = mounts.get(label)
if (!ent) {
return { drive: systemDrive, path: absPath }
}
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
const p = unixPathResolve('/', sub)
return { drive: ent.drive, path: p, mntReadOnly: !ent.writable }
}
/**
* Map logical absolute path to { drive, path } for Hyperdrive ops.
* Virtual /home lists only the active session dir; /home/<active>/… is the personal drive.
*/
function routeSystemRoAlias(absPath) {
if (!systemRoAliasNorm || !systemRoAliasNorm.startsWith('/')) return null
const norm = absPath.replace(/\/+$/, '') || '/'
if (
norm !== systemRoAliasNorm &&
!absPath.startsWith(systemRoAliasNorm + '/')
)
return null
let tail = '/'
if (norm !== systemRoAliasNorm) {
tail = absPath.slice(systemRoAliasNorm.length) || '/'
}
const p = tail.startsWith('/') ? tail : '/' + tail
const resolved = unixPathResolve('/', p.replace(/^\/+/, '') || '/')
return {
drive: systemDrive,
path: resolved,
mntReadOnly: true
}
}
/**
* When HDMS (or other) mounts label `www` at `/mnt/www`, expose it at `$HOME/.www`.
* @param {string} relFromHomeRoot path under the session home root (e.g. `.www`, `.www/a`)
* @returns {{ drive: import('hyperdrive').default, path: string, mntReadOnly: boolean } | null}
*/
function routeHomeWwwAlias(relFromHomeRoot) {
const rel = String(relFromHomeRoot || '').replace(/^\/+/, '')
if (rel !== '.www' && !rel.startsWith('.www/')) return null
const mounts = getMntMap()
const ent = mounts.get('www')
if (!ent) return null
const wwwPrefix = '.www/'
const tail =
rel === '.www'
? ''
: rel.startsWith(wwwPrefix)
? rel.slice(wwwPrefix.length)
: null
if (tail === null && rel !== '.www') return null
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
const p = unixPathResolve('/', sub)
return {
drive: ent.drive,
path: p,
mntReadOnly: !ent.writable
}
}
function route(absPath) {
const aliasR = routeSystemRoAlias(absPath)
if (aliasR) return aliasR
@@ -72,6 +72,20 @@ import {
} from './lib/vfs-policy.js'
import { createVfsWarmReadCache } from './lib/vfs-warm-cache.js'
import { createSessionReadMaskedLine } from './lib/cli-readline.js'
import {
bareOsPipelineChildCtx,
bareOsPipelineRawChunkToText,
runWithShellPipelineStageTimeout
} from './lib/shell-pipeline.js'
import {
assignShellFunctionPositionalEnv,
tryReportMisplacedReservedStatementStart,
casePatternList,
execDoubleBracketLimited,
bareOsResetShellIdentityState
} from './lib/shell-stmt.js'
import { createVfsRouteHelpers } from './lib/vfs-route.js'
import { createBareOsPosixFdSimMethods } from './lib/bare-os-posix-fd-sim.js'
test('posix env flags and caps', (t) => {
t.ok(wantPosixSocketFdBridge({ BARE_OS_POSIX_SOCKET_FD_BRIDGE: '1' }))
@@ -474,3 +488,97 @@ test('session masked line falls back without TTY', async (t) => {
})
t.is(await read('pw> '), 'fb:pw> ')
})
test('pipeline child ctx chunk and timeout', async (t) => {
const child = bareOsPipelineChildCtx({ a: 1 }, { X: '1' }, 'in', true)
t.is(child.shellStdin, 'in')
t.ok(child.bareOsStdoutCaptured)
t.is(child.env.X, '1')
t.is(bareOsPipelineRawChunkToText('ab'), 'ab')
t.is(bareOsPipelineRawChunkToText(new Uint8Array([65, 66])), 'AB')
const v = await runWithShellPipelineStageTimeout({}, async () => 7, 'x')
t.is(v, 7)
})
test('shell stmt helpers local-style', async (t) => {
const env = {}
assignShellFunctionPositionalEnv(env, ['fn', 'a', 'b'])
t.is(env['0'], 'fn')
t.is(env['1'], 'a')
t.is(env['2'], 'b')
t.is(env['#'], '2')
const errs = []
const ctx = { console: { error: (m) => errs.push(m) }, exitCode: 0 }
t.ok(tryReportMisplacedReservedStatementStart(ctx, [{ type: 'word', value: 'then' }]))
t.is(ctx.exitCode, 2)
t.alike(casePatternList([{ type: 'word', value: 'x*' }, { type: 'op', value: '|' }, { type: 'word', value: 'y' }], {}), [
'x*',
'y'
])
const br = {
vfs: { env: { A: 'hi' } },
console: { error: () => {} },
exitCode: 99
}
await execDoubleBracketLimited(br, [
{ type: 'word', value: '[[' },
{ type: 'word', value: '$A' },
{ type: 'word', value: '==' },
{ type: 'word', value: 'hi' },
{ type: 'word', value: ']]' }
])
t.is(br.exitCode, 0)
const jobs = { shellBackgroundJobs: { nextId: 9, list: [1] } }
bareOsResetShellIdentityState(jobs)
t.is(jobs.shellBackgroundJobs.nextId, 1)
t.is(jobs.shellBackgroundJobs.list.length, 0)
})
test('vfs route tilde mnt and ro alias', (t) => {
const helpers = createVfsRouteHelpers({
getCwd: () => '/home/g',
normalizeHome: () => '/home/g',
mntRef: {
getMounts: () =>
new Map([['data', { drive: { get: () => 1 }, writable: true }]])
},
systemRoAliasNorm: '/sysro',
systemDrive: { id: 'sys' }
})
t.is(helpers.expandTilde('~/x'), '/home/g/x')
t.is(helpers.resolveLogical('y'), '/home/g/y')
t.ok(helpers.routeMnt('/mnt').virtualMntRoot)
t.is(helpers.routeMnt('/mnt/data/a').path, '/a')
t.ok(helpers.routeSystemRoAlias('/sysro/etc').mntReadOnly)
t.ok(helpers.pseudoMountsText().includes('/mnt/data'))
})
test('posix fd sim pipe write read', (t) => {
const methods = createBareOsPosixFdSimMethods({
env: { BARE_OS_POSIX_FD_SIM: '1' },
bootHrtimeNowNs: null
})
const ctx = {
bareOsLogicalFds: {},
bareOsLogicalFdFlags: {},
bareOsPosixFdSimState: {
nextFd: 10,
nextPairId: 1,
queues: Object.create(null),
byteTotals: Object.create(null)
},
bareOsRegisterLogicalFd(fd, target) {
this.bareOsLogicalFds[String(fd)] = target
},
...methods
}
const pipe = ctx.bareOsPosixFdSimPipe()
t.ok(pipe)
const w = ctx.bareOsPosixFdSimWrite(pipe.writeFd, 'hi')
t.ok(w.ok)
const r = ctx.bareOsPosixFdSimRead(pipe.readFd)
t.ok(r.ok)
t.is(r.data, 'hi')
const probe = ctx.bareOsPosixPollProbe({ fds: [{ fd: 1, events: 'w' }] })
t.ok(probe.ready.some((x) => x.fd === 1 && x.revents === 'w'))
})