This commit is contained in:
2026-08-18 17:57:59 -04:00
parent d5e9798645
commit c072d24bb1
9 changed files with 822 additions and 591 deletions
+8 -212
View File
@@ -167,6 +167,8 @@ 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 { createBareOsPathconfMethods } from './lib/bare-os-pathconf.js'
import { createBareOsLogicalFdMethods } from './lib/bare-os-logical-fd.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'
@@ -4462,224 +4464,18 @@ async function executeKernel(disk, store, swarm, initSource) {
errorCode: 'host_subprocess_unavailable'
}
},
/**
* @param {number} fd
* @param {string} target
*/
bareOsRegisterLogicalFd(fd, target) {
const n = Number(fd)
if (!Number.isFinite(n) || n < 0 || n > 65535) return
const k = String(n >>> 0)
if (k === '0' || k === '1' || k === '2') return
this.bareOsLogicalFds[k] = String(target || '')
},
/** @param {number} fd */
bareOsUnregisterLogicalFd(fd) {
const k = String(Number(fd) >>> 0)
if (this.bareOsLogicalFds && typeof this.bareOsLogicalFds === 'object') {
delete this.bareOsLogicalFds[k]
}
if (
this.bareOsLogicalFdFlags &&
typeof this.bareOsLogicalFdFlags === 'object'
) {
delete this.bareOsLogicalFdFlags[k]
}
},
...createBareOsLogicalFdMethods(),
...createBareOsPosixFdSimMethods({
env: shellEnv,
bootHrtimeNowNs
}),
/**
* Logical `sigaction`: `IGNORE` suppresses synthetic delivery for this signal.
* @param {string} signal
* @param {'IGNORE' | 'DEFAULT' | 'IGN' | 'DEF'} mode
*/
bareOsSigaction(signal, mode) {
const s = bareOsNormalizeSignalName(signal)
if (!bareOsIsPosixSignalName(signal) || s === '0') {
throw new Error('bareOsSigaction: unsupported signal')
}
const m = String(mode || 'DEFAULT')
.toUpperCase()
.replace(/^SIG/i, '')
if (m === 'DEFAULT' || m === 'DEF') {
if (this.bareOsLogicalSigaction) delete this.bareOsLogicalSigaction[s]
return { ok: true, signal: s, mode: 'DEFAULT' }
}
if (m === 'IGNORE' || m === 'IGN') {
if (!this.bareOsLogicalSigaction) {
throw new Error('bareOsSigaction: internal state missing')
}
this.bareOsLogicalSigaction[s] = 'IGNORE'
return { ok: true, signal: s, mode: 'IGNORE' }
}
throw new Error('bareOsSigaction: mode must be IGNORE or DEFAULT')
},
/** @type {Record<string, unknown>[]} */
bareOsSnapshotHandles: [],
/** @param {Record<string, unknown>} desc */
bareOsRegisterSnapshotHandle(desc) {
if (!desc || typeof desc !== 'object') return
if (!Array.isArray(this.bareOsSnapshotHandles)) {
this.bareOsSnapshotHandles = []
}
const id =
typeof desc.id === 'string' && desc.id.trim()
? desc.id.trim().slice(0, 128)
: `snap-${this.bareOsSnapshotHandles.length}`
this.bareOsSnapshotHandles.push({ ...desc, id, atMs: Date.now() })
},
/**
* Fixed pathconf/nameconf-style limits (no live host query).
* @param {string} [_path]
* @param {string} name
*/
bareOsPathconf(pathArg, name) {
const k = String(name || '').trim()
const p = vfs.resolveLogical(String(pathArg || '/').trim() || '/')
const rawUnion = String(shellEnv.BARE_OS_VFS_UNION_PREFIXES || '')
const underUnion = rawUnion
.split(',')
.map((s) => s.trim())
.filter((x) => x.startsWith('/'))
.some((pre) => p === pre || p.startsWith(pre + '/'))
const underMirror = p === '/mirror' || p.startsWith('/mirror/')
const acctOn =
shellEnv.BARE_OS_PERSONAL_ACCT_PREFIX === '1' ||
shellEnv.BARE_OS_PERSONAL_ACCT_PREFIX === 'true'
const underPersonalAcct =
acctOn && (p === '/.bare-os/acct' || p.startsWith('/.bare-os/acct/'))
const underDevShm = p === '/dev/shm' || p.startsWith('/dev/shm/')
/** @type {Record<string, number>} */
const table = {
PATH_MAX: 4096,
NAME_MAX: 255,
_PC_PATH_MAX: 4096,
_PC_NAME_MAX: 255,
_PC_SYMLINK_MAX: 1024,
_PC_CHOWN_RESTRICTED: 1,
_PC_NO_TRUNC: 1,
_PC_FILESIZEBITS: 64,
_PC_LINK_MAX: 1,
_PC_MAX_CANON: 255,
_PC_MAX_INPUT: 512,
_PC_PIPE_BUF: 4096,
_PC_2_SYMLINKS: 1,
_POSIX_VERSION: 200809,
_POSIX_THREAD_ATTR_STACKSIZE: 65536
}
if (k === '_PC_CHOWN_RESTRICTED') {
if (underMirror) return 0
return 1
}
if (
k === '_PC_NO_TRUNC' &&
(underMirror || (underUnion && !underPersonalAcct))
) {
return 0
}
if (underDevShm && k === '_PC_NAME_MAX') {
return 128
}
if (!k || table[k] === undefined) {
throw new Error('bareOsPathconf: unknown name: ' + k)
}
return table[k]
},
/**
* Dynamic **`sysconf`**-style values for **`getconf _SC_*`** names (not path-specific).
* @param {string} name
* @returns {string | number | null}
*/
bareOsGetconfSysconf(name) {
const n = String(name || '').trim()
if (n === '_SC_NPROCESSORS_ONLN') {
const raw = String(shellEnv.BARE_OS_NPROC || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(Math.min(4096, p))
return '4'
}
if (n === '_SC_PAGESIZE' || n === '_SC_PAGE_SIZE') return '4096'
if (n === '_SC_PHYS_PAGES') {
const raw = String(shellEnv.BARE_OS_PHYS_PAGES_HINT || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(p)
return '524288'
}
if (n === '_SC_AVPHYS_PAGES') {
const raw = String(shellEnv.BARE_OS_AVPHYS_PAGES_HINT || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(p)
return '262144'
}
if (n === '_SC_OPEN_MAX') return '256'
if (n === '_SC_STREAM_MAX') return '256'
if (n === '_SC_CHILD_MAX') return '0'
if (n === '_SC_CLK_TCK') return '100'
if (n === '_SC_ARG_MAX') return '262144'
if (n === '_SC_NPROCESSORS_CONF') {
const raw = String(shellEnv.BARE_OS_NPROC || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(Math.min(4096, p))
return '4'
}
if (n === '_SC_HOST_NAME_MAX' || n === '_POSIX_HOST_NAME_MAX')
return '255'
if (n === '_SC_SYMLOOP_MAX') return '32'
if (n === '_SC_IOV_MAX' || n === '_SC_UIO_MAXIOV') return '1024'
if (n === '_SC_THREAD_DESTRUCTOR_ITERATIONS') return '4'
if (n === '_SC_TTY_NAME_MAX') return '32'
if (n === '_SC_LOGIN_NAME_MAX') return '256'
if (n === '_SC_MONOTONIC_CLOCK') return '1'
if (n === '_SC_MONOTONIC_CLOCK_RES') {
return bootHrtimeNowNs ? '1' : '0'
}
if (n === '_SC_TRACE' || n === '_SC_TRACE_VERSION') return '-1'
if (n === '_SC_RAW_SOCKETS') return '-1'
if (n === '_SC_ADVISORY_INFO') return '0'
if (n === '_SC_BARRIERS') return '-1'
if (n === '_SC_SPIN_LOCKS') return '1'
if (n === '_SC_TIMER_MAX') return '32'
if (n === '_SC_DELAYTIMER_MAX') return '0'
if (n === '_SC_ATEXIT_MAX') return '32'
if (n === '_SC_LINE_MAX') return '2048'
if (n === '_SC_BC_BASE_MAX') return '99'
if (n === '_SC_BC_DIM_MAX') return '2048'
if (n === '_SC_BC_SCALE_MAX') return '99'
const pl = getBareOsPipelineLimits(shellEnv)
if (n === '_SC_BARE_OS_PIPELINE_MAX_BYTES') return String(pl.maxBytes)
if (n === '_SC_BARE_OS_PIPELINE_MAX_LINES') return String(pl.maxLines)
if (n === '_SC_BARE_OS_PIPELINE_MAX_STAGES') return String(pl.maxStages)
const parseCap = (key, def, cap) => {
const raw = String(shellEnv[key] ?? '').trim()
if (!raw) return String(def)
const p = parseInt(raw, 10)
if (!Number.isFinite(p) || p < 0) return String(def)
return String(cap != null ? Math.min(cap, p) : p)
}
if (n === '_SC_BARE_OS_GLOB_MAX_MATCHES')
return parseCap('BARE_OS_GLOB_MAX_MATCHES', 4096, null)
if (n === '_SC_BARE_OS_SHELL_LOOP_MAX')
return parseCap('BARE_OS_SHELL_LOOP_MAX', 10000, null)
if (n === '_SC_BARE_OS_EXEC_MAX_DEPTH')
return parseCap('BARE_OS_EXEC_MAX_DEPTH', 64, null)
if (n === '_SC_BARE_OS_XARGS_MAX_PROCS')
return parseCap('BARE_OS_XARGS_MAX_PROCS', 8, 32)
if (n === '_SC_BARE_OS_SWARM_MAX_PEERS')
return parseCap('BARE_OS_SWARM_MAX_PEERS', 512, null)
if (n === '_SC_BARE_OS_SWARM_MAX_CLIENT_CONNECTIONS')
return parseCap('BARE_OS_SWARM_MAX_CLIENT_CONNECTIONS', 256, null)
if (n === '_SC_BARE_OS_SWARM_MAX_SERVER_CONNECTIONS')
return parseCap('BARE_OS_SWARM_MAX_SERVER_CONNECTIONS', 256, null)
if (n === '_SC_BARE_OS_SWARM_MAX_PARALLEL')
return parseCap('BARE_OS_SWARM_MAX_PARALLEL', 64, null)
if (n === '_SC_BARE_OS_DGRAM_RECVQ_MAX')
return parseCap('BARE_OS_POSIX_DGRAM_RECVQ_MAX', 256, null)
if (n === '_SC_BARE_OS_ACCEPT_QUEUE_MAX')
return parseCap('BARE_OS_POSIX_ACCEPT_QUEUE_MAX', 64, null)
return null
},
...createBareOsPathconfMethods({
env: shellEnv,
resolveLogical: (p) => vfs.resolveLogical(p),
bootHrtimeNowNs
}),
bareOsSyscall: runBareOsSyscall,
/**
* Peer admission against optional **`BARE_OS_PEER_DENYLIST_HEX`**,
@@ -0,0 +1,74 @@
/**
* Logical FD registry, sigaction, and snapshot-handle methods on kernel `ctx`.
*/
import {
bareOsIsPosixSignalName,
bareOsNormalizeSignalName
} from './bare-os-posix-signals.js'
export function createBareOsLogicalFdMethods() {
return {
/**
* @param {number} fd
* @param {string} target
*/
bareOsRegisterLogicalFd(fd, target) {
const n = Number(fd)
if (!Number.isFinite(n) || n < 0 || n > 65535) return
const k = String(n >>> 0)
if (k === '0' || k === '1' || k === '2') return
this.bareOsLogicalFds[k] = String(target || '')
},
/** @param {number} fd */
bareOsUnregisterLogicalFd(fd) {
const k = String(Number(fd) >>> 0)
if (this.bareOsLogicalFds && typeof this.bareOsLogicalFds === 'object') {
delete this.bareOsLogicalFds[k]
}
if (
this.bareOsLogicalFdFlags &&
typeof this.bareOsLogicalFdFlags === 'object'
) {
delete this.bareOsLogicalFdFlags[k]
}
},
/**
* Logical `sigaction`: `IGNORE` suppresses synthetic delivery for this signal.
* @param {string} signal
* @param {'IGNORE' | 'DEFAULT' | 'IGN' | 'DEF'} mode
*/
bareOsSigaction(signal, mode) {
const s = bareOsNormalizeSignalName(signal)
if (!bareOsIsPosixSignalName(signal) || s === '0') {
throw new Error('bareOsSigaction: unsupported signal')
}
const m = String(mode || 'DEFAULT')
.toUpperCase()
.replace(/^SIG/i, '')
if (m === 'DEFAULT' || m === 'DEF') {
if (this.bareOsLogicalSigaction) delete this.bareOsLogicalSigaction[s]
return { ok: true, signal: s, mode: 'DEFAULT' }
}
if (m === 'IGNORE' || m === 'IGN') {
if (!this.bareOsLogicalSigaction) {
throw new Error('bareOsSigaction: internal state missing')
}
this.bareOsLogicalSigaction[s] = 'IGNORE'
return { ok: true, signal: s, mode: 'IGNORE' }
}
throw new Error('bareOsSigaction: mode must be IGNORE or DEFAULT')
},
/** @param {Record<string, unknown>} desc */
bareOsRegisterSnapshotHandle(desc) {
if (!desc || typeof desc !== 'object') return
if (!Array.isArray(this.bareOsSnapshotHandles)) {
this.bareOsSnapshotHandles = []
}
const id =
typeof desc.id === 'string' && desc.id.trim()
? desc.id.trim().slice(0, 128)
: `snap-${this.bareOsSnapshotHandles.length}`
this.bareOsSnapshotHandles.push({ ...desc, id, atMs: Date.now() })
}
}
}
@@ -0,0 +1,168 @@
/**
* ctx.bareOsPathconf / ctx.bareOsGetconfSysconf implementations.
*/
import { getBareOsPipelineLimits } from './shell-runtime.js'
/**
* @param {{
* env: Record<string, string | undefined>,
* resolveLogical: (pathArg: string) => string,
* bootHrtimeNowNs?: (() => bigint) | null
* }} deps
*/
export function createBareOsPathconfMethods(deps) {
const { env, resolveLogical, bootHrtimeNowNs = null } = deps
return {
/**
* Fixed pathconf/nameconf-style limits (no live host query).
* @param {string} [_path]
* @param {string} name
*/
bareOsPathconf(pathArg, name) {
const k = String(name || '').trim()
const p = resolveLogical(String(pathArg || '/').trim() || '/')
const rawUnion = String(env.BARE_OS_VFS_UNION_PREFIXES || '')
const underUnion = rawUnion
.split(',')
.map((s) => s.trim())
.filter((x) => x.startsWith('/'))
.some((pre) => p === pre || p.startsWith(pre + '/'))
const underMirror = p === '/mirror' || p.startsWith('/mirror/')
const acctOn =
env.BARE_OS_PERSONAL_ACCT_PREFIX === '1' ||
env.BARE_OS_PERSONAL_ACCT_PREFIX === 'true'
const underPersonalAcct =
acctOn && (p === '/.bare-os/acct' || p.startsWith('/.bare-os/acct/'))
const underDevShm = p === '/dev/shm' || p.startsWith('/dev/shm/')
/** @type {Record<string, number>} */
const table = {
PATH_MAX: 4096,
NAME_MAX: 255,
_PC_PATH_MAX: 4096,
_PC_NAME_MAX: 255,
_PC_SYMLINK_MAX: 1024,
_PC_CHOWN_RESTRICTED: 1,
_PC_NO_TRUNC: 1,
_PC_FILESIZEBITS: 64,
_PC_LINK_MAX: 1,
_PC_MAX_CANON: 255,
_PC_MAX_INPUT: 512,
_PC_PIPE_BUF: 4096,
_PC_2_SYMLINKS: 1,
_POSIX_VERSION: 200809,
_POSIX_THREAD_ATTR_STACKSIZE: 65536
}
if (k === '_PC_CHOWN_RESTRICTED') {
if (underMirror) return 0
return 1
}
if (
k === '_PC_NO_TRUNC' &&
(underMirror || (underUnion && !underPersonalAcct))
) {
return 0
}
if (underDevShm && k === '_PC_NAME_MAX') {
return 128
}
if (!k || table[k] === undefined) {
throw new Error('bareOsPathconf: unknown name: ' + k)
}
return table[k]
},
/**
* Dynamic **`sysconf`**-style values for **`getconf _SC_*`** names (not path-specific).
* @param {string} name
* @returns {string | number | null}
*/
bareOsGetconfSysconf(name) {
const n = String(name || '').trim()
if (n === '_SC_NPROCESSORS_ONLN') {
const raw = String(env.BARE_OS_NPROC || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(Math.min(4096, p))
return '4'
}
if (n === '_SC_PAGESIZE' || n === '_SC_PAGE_SIZE') return '4096'
if (n === '_SC_PHYS_PAGES') {
const raw = String(env.BARE_OS_PHYS_PAGES_HINT || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(p)
return '524288'
}
if (n === '_SC_AVPHYS_PAGES') {
const raw = String(env.BARE_OS_AVPHYS_PAGES_HINT || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(p)
return '262144'
}
if (n === '_SC_OPEN_MAX') return '256'
if (n === '_SC_STREAM_MAX') return '256'
if (n === '_SC_CHILD_MAX') return '0'
if (n === '_SC_CLK_TCK') return '100'
if (n === '_SC_ARG_MAX') return '262144'
if (n === '_SC_NPROCESSORS_CONF') {
const raw = String(env.BARE_OS_NPROC || '').trim()
const p = parseInt(raw, 10)
if (Number.isFinite(p) && p > 0) return String(Math.min(4096, p))
return '4'
}
if (n === '_SC_HOST_NAME_MAX' || n === '_POSIX_HOST_NAME_MAX')
return '255'
if (n === '_SC_SYMLOOP_MAX') return '32'
if (n === '_SC_IOV_MAX' || n === '_SC_UIO_MAXIOV') return '1024'
if (n === '_SC_THREAD_DESTRUCTOR_ITERATIONS') return '4'
if (n === '_SC_TTY_NAME_MAX') return '32'
if (n === '_SC_LOGIN_NAME_MAX') return '256'
if (n === '_SC_MONOTONIC_CLOCK') return '1'
if (n === '_SC_MONOTONIC_CLOCK_RES') {
return bootHrtimeNowNs ? '1' : '0'
}
if (n === '_SC_TRACE' || n === '_SC_TRACE_VERSION') return '-1'
if (n === '_SC_RAW_SOCKETS') return '-1'
if (n === '_SC_ADVISORY_INFO') return '0'
if (n === '_SC_BARRIERS') return '-1'
if (n === '_SC_SPIN_LOCKS') return '1'
if (n === '_SC_TIMER_MAX') return '32'
if (n === '_SC_DELAYTIMER_MAX') return '0'
if (n === '_SC_ATEXIT_MAX') return '32'
if (n === '_SC_LINE_MAX') return '2048'
if (n === '_SC_BC_BASE_MAX') return '99'
if (n === '_SC_BC_DIM_MAX') return '2048'
if (n === '_SC_BC_SCALE_MAX') return '99'
const pl = getBareOsPipelineLimits(env)
if (n === '_SC_BARE_OS_PIPELINE_MAX_BYTES') return String(pl.maxBytes)
if (n === '_SC_BARE_OS_PIPELINE_MAX_LINES') return String(pl.maxLines)
if (n === '_SC_BARE_OS_PIPELINE_MAX_STAGES') return String(pl.maxStages)
const parseCap = (key, def, cap) => {
const raw = String(env[key] ?? '').trim()
if (!raw) return String(def)
const p = parseInt(raw, 10)
if (!Number.isFinite(p) || p < 0) return String(def)
return String(cap != null ? Math.min(cap, p) : p)
}
if (n === '_SC_BARE_OS_GLOB_MAX_MATCHES')
return parseCap('BARE_OS_GLOB_MAX_MATCHES', 4096, null)
if (n === '_SC_BARE_OS_SHELL_LOOP_MAX')
return parseCap('BARE_OS_SHELL_LOOP_MAX', 10000, null)
if (n === '_SC_BARE_OS_EXEC_MAX_DEPTH')
return parseCap('BARE_OS_EXEC_MAX_DEPTH', 64, null)
if (n === '_SC_BARE_OS_XARGS_MAX_PROCS')
return parseCap('BARE_OS_XARGS_MAX_PROCS', 8, 32)
if (n === '_SC_BARE_OS_SWARM_MAX_PEERS')
return parseCap('BARE_OS_SWARM_MAX_PEERS', 512, null)
if (n === '_SC_BARE_OS_SWARM_MAX_CLIENT_CONNECTIONS')
return parseCap('BARE_OS_SWARM_MAX_CLIENT_CONNECTIONS', 256, null)
if (n === '_SC_BARE_OS_SWARM_MAX_SERVER_CONNECTIONS')
return parseCap('BARE_OS_SWARM_MAX_SERVER_CONNECTIONS', 256, null)
if (n === '_SC_BARE_OS_SWARM_MAX_PARALLEL')
return parseCap('BARE_OS_SWARM_MAX_PARALLEL', 64, null)
if (n === '_SC_BARE_OS_DGRAM_RECVQ_MAX')
return parseCap('BARE_OS_POSIX_DGRAM_RECVQ_MAX', 256, null)
if (n === '_SC_BARE_OS_ACCEPT_QUEUE_MAX')
return parseCap('BARE_OS_POSIX_ACCEPT_QUEUE_MAX', 64, null)
return null
}
}
}
+68
View File
@@ -179,3 +179,71 @@ export function bareOsResetShellIdentityState(ctx) {
ctx.shellBackgroundJobs.nextId = 1
}
}
/**
* Consume an interactive here-document after `cmd <<DELIM`.
* @param {Record<string, unknown>} ctx
* @param {string} execLine
* @param {(ctx: Record<string, unknown>) => void} syncExit
* @returns {Promise<{ ok: true, execLine: string } | { ok: false }>}
*/
export async function consumeInteractiveHeredoc(ctx, execLine, syncExit) {
const readL = ctx.readLine
if (typeof readL !== 'function') return { ok: true, execLine }
const hm = execLine.match(/^(.*?)<<-?\s*(?:'([^']+)'|"([^"]+)"|(\S+))\s*$/)
if (!hm) return { ok: true, execLine }
const prefix = hm[1].trimEnd()
if (!prefix) {
ctx.console.error(
'shell: here-document requires a command before << on the same line'
)
ctx.exitCode = 2
syncExit(ctx)
return { ok: false }
}
const delim = hm[2] ?? hm[3] ?? hm[4]
const singleQuoted = hm[2] != null
const posixHeredocCap = (() => {
if (
ctx.env?.BARE_OS_SHELL_POSIX_MODE !== '1' &&
ctx.env?.BARE_OS_SHELL_POSIX_MODE !== 'true'
) {
return null
}
const raw = String(ctx.env.BARE_OS_SHELL_HEREDOC_MAX_BYTES || '').trim()
const n = parseInt(raw, 10)
if (Number.isFinite(n) && n > 0) return Math.min(2_000_000, n)
return 262144
})()
/** @type {string[]} */
const bodyLines = []
let heredocAcc = 0
for (;;) {
const ln = await readL('> ')
if (ln == null) break
if (ln === delim) break
if (posixHeredocCap != null) {
heredocAcc += ln.length + 1
if (heredocAcc > posixHeredocCap) {
ctx.console.error(
'shell: here-document exceeds BARE_OS_SHELL_HEREDOC_MAX_BYTES cap (POSIX mode)'
)
ctx.exitCode = 2
syncExit(ctx)
return { ok: false }
}
}
bodyLines.push(ln)
}
const vfs = ctx.vfs
const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {}
let body = bodyLines.join('\n')
if (!singleQuoted) {
body = bodyLines.map((l) => expandWord(l, env)).join('\n')
}
ctx.shellHeredocOnce = body
if (body.length > 0 && !body.endsWith('\n')) {
ctx.shellHeredocOnce += '\n'
}
return { ok: true, execLine: prefix }
}
+10 -60
View File
@@ -65,7 +65,8 @@ import {
tryReportMisplacedReservedStatementStart,
execDoubleBracketLimited,
assignShellFunctionPositionalEnv,
casePatternList
casePatternList,
consumeInteractiveHeredoc
} from './shell-stmt.js'
export { BARE_OS_SHELL_NOUNSET_ERROR }
@@ -1987,65 +1988,14 @@ async function execShellLineInner(ctx, rawTrimmed) {
}
}
ctx.shellSessionState.foregroundPgid = ctx.shellSessionState.sid
const readL = ctx.readLine
if (typeof readL === 'function') {
const hm = execLine.match(/^(.*?)<<-?\s*(?:'([^']+)'|"([^"]+)"|(\S+))\s*$/)
if (hm) {
const prefix = hm[1].trimEnd()
if (!prefix) {
ctx.console.error(
'shell: here-document requires a command before << on the same line'
)
ctx.exitCode = 2
syncBareOsExitStatusEnv(ctx)
return 'ok'
}
const delim = hm[2] ?? hm[3] ?? hm[4]
const singleQuoted = hm[2] != null
const posixHeredocCap = (() => {
if (
ctx.env?.BARE_OS_SHELL_POSIX_MODE !== '1' &&
ctx.env?.BARE_OS_SHELL_POSIX_MODE !== 'true'
) {
return null
}
const raw = String(ctx.env.BARE_OS_SHELL_HEREDOC_MAX_BYTES || '').trim()
const n = parseInt(raw, 10)
if (Number.isFinite(n) && n > 0) return Math.min(2_000_000, n)
return 262144
})()
/** @type {string[]} */
const bodyLines = []
let heredocAcc = 0
for (;;) {
const ln = await readL('> ')
if (ln == null) break
if (ln === delim) break
if (posixHeredocCap != null) {
heredocAcc += ln.length + 1
if (heredocAcc > posixHeredocCap) {
ctx.console.error(
'shell: here-document exceeds BARE_OS_SHELL_HEREDOC_MAX_BYTES cap (POSIX mode)'
)
ctx.exitCode = 2
syncBareOsExitStatusEnv(ctx)
return 'ok'
}
}
bodyLines.push(ln)
}
const vfs = ctx.vfs
const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {}
let body = bodyLines.join('\n')
if (!singleQuoted) {
body = bodyLines.map((l) => expandWord(l, env)).join('\n')
}
ctx.shellHeredocOnce = body
if (body.length > 0 && !body.endsWith('\n')) {
ctx.shellHeredocOnce += '\n'
}
execLine = prefix
}
{
const heredoc = await consumeInteractiveHeredoc(
ctx,
execLine,
syncBareOsExitStatusEnv
)
if (!heredoc.ok) return 'ok'
execLine = heredoc.execLine
}
const tokens = tokenize(execLine)
@@ -0,0 +1,203 @@
/**
* Personal-drive physical lstat / put helpers used by createVfs.
*/
import { dirnameAbs, pathPrefixes } from './vfs-path.js'
import {
extractBareOs,
mergeBareOsOnWrite,
mergeEntryMetadata,
modeAllows,
parseUidGid,
statFromBareOs,
synthesizeStat,
S_IFDIR
} from './vfs-posix-meta.js'
import { bareOsVfsAclDeniesDriveOp } from './bare-os-vfs-acl-enforce.js'
/**
* @param {{
* personalDrive: unknown,
* env: Record<string, string | undefined>,
* DIR_MARKER: string,
* entryOn: (drive: unknown, p: string, opts?: Record<string, unknown>) => Promise<unknown>,
* statFromEntryValue: (abs: string, r: unknown, personal: boolean, e: unknown) => unknown,
* assertNotBootPolicyDenyVfs: (abs: string, op: string) => void,
* assertUnionWriteNotDenied: (abs: string) => void,
* assertGuestSensitivePersonalOp: (drive: unknown, personalPath: string, op: string, logicalAbs: string) => void
* }} deps
*/
export function createVfsPersonalDriveIo(deps) {
const {
personalDrive,
env,
DIR_MARKER,
entryOn,
statFromEntryValue,
assertNotBootPolicyDenyVfs,
assertUnionWriteNotDenied,
assertGuestSensitivePersonalOp
} = deps
/**
* `/.bare/holesail/**` on the personal drive: parent checks and traverse must walk **physical**
* paths on that drive — logical `/.bare` may map to a different backing path under
* account prefix, and `/` is skipped as a virtual mount point.
*/
async function lstatPersonalDrivePhysicalDir(physPath) {
if (!personalDrive) return null
const pnorm = String(physPath || '').replace(/\/+$/, '') || '/'
if (pnorm === '/') {
return { ...synthesizeStat('/', true, env, 'directory'), path: pnorm }
}
const e = await entryOn(personalDrive, pnorm, { follow: false })
const fromVal = statFromEntryValue(
pnorm,
{ drive: personalDrive, path: pnorm },
true,
e
)
if (fromVal) return { ...fromVal, path: pnorm }
const names = []
try {
const stream = personalDrive.readdir(pnorm)
for await (const n of stream) names.push(n)
} catch {
/* missing parent */
}
if (names.length) {
if (names.includes(DIR_MARKER)) {
const mPath = `${pnorm}/${DIR_MARKER}`.replace(/\/{2,}/g, '/')
const me = await entryOn(personalDrive, mPath, { follow: false })
const mv = me?.value
if (mv?.blob) {
const bo = extractBareOs(mv)
if (bo) {
let perm = bo.mode & 0o777
if (perm & 0o400) perm |= 0o100
if (perm & 0o040) perm |= 0o010
if (perm & 0o004) perm |= 0o001
return statFromBareOs(
{ ...bo, mode: S_IFDIR | perm },
'directory',
0,
pnorm
)
}
}
}
return { ...synthesizeStat(pnorm, true, env, 'directory'), path: pnorm }
}
if (e) return { ...synthesizeStat(pnorm, true, env, 'directory'), path: pnorm }
return null
}
/** Directory or regular file (or symlink) on the personal drive by absolute path on that drive. */
async function lstatPersonalDrivePhysicalAny(physPath) {
if (!personalDrive) return null
const pnorm = String(physPath || '').replace(/\/+$/, '') || '/'
if (pnorm === '/') {
return { ...synthesizeStat('/', true, env, 'directory'), path: pnorm }
}
const e = await entryOn(personalDrive, pnorm, { follow: false })
const rf = statFromEntryValue(
pnorm,
{ drive: personalDrive, path: pnorm },
true,
e
)
if (rf) return rf
return await lstatPersonalDrivePhysicalDir(pnorm)
}
async function assertPersonalDriveAncestorWritableForCreate(physPath) {
if (!personalDrive) {
throw new Error('ENOENT: ' + String(physPath || ''))
}
const { uid: euid, gid: egid } = parseUidGid(env)
const norm = String(physPath || '').replace(/\/+$/, '') || '/'
let probe = dirnameAbs(norm)
/** @type {{ pre: string, st: unknown } | null} */
let deepest = null
while (probe && probe !== '/') {
const st = await lstatPersonalDrivePhysicalDir(probe)
if (st && st.type === 'directory') {
deepest = { pre: probe, st }
break
}
probe = dirnameAbs(probe)
}
if (!deepest) {
deepest = {
pre: '/',
st: await lstatPersonalDrivePhysicalDir('/')
}
}
if (!deepest.st || !modeAllows(deepest.st, euid, egid, 'w', 'directory')) {
throw new Error('EACCES: cannot create in ' + String(deepest?.pre ?? ''))
}
}
/**
* @param {string} logicalAbs policy / guest messaging
* @param {string} physPath path on personalDrive
*/
async function putPersonalDrivePhysical(logicalAbs, physPath, buf, opts = {}) {
assertNotBootPolicyDenyVfs(logicalAbs, 'write')
assertUnionWriteNotDenied(logicalAbs)
if (!personalDrive) {
throw new Error('Read-only path (not under $HOME): ' + logicalAbs)
}
if (await bareOsVfsAclDeniesDriveOp(env, personalDrive, physPath, 'write')) {
throw new Error('EACCES: ACL enforces deny write: ' + logicalAbs)
}
assertGuestSensitivePersonalOp(personalDrive, physPath, 'write', logicalAbs)
const existing = await entryOn(personalDrive, physPath, { follow: false })
const hadBlob = !!existing?.value?.blob
if (!hadBlob) await assertPersonalDriveAncestorWritableForCreate(physPath)
const value = existing?.value
const prevBare = extractBareOs(value)
const bareOs = mergeBareOsOnWrite(prevBare, env, {
executable: opts.executable,
bumpMtime: opts.bumpMtime !== false,
touchCtime: opts.touchCtime === true,
legacyExecutable: !!value?.executable,
mtimeMs: opts.mtimeMs,
ctimeMs: opts.ctimeMs,
posixModeBits: opts.posixModeBits
})
const executable =
opts.executable !== undefined ? !!opts.executable : !!value?.executable
const metadata = mergeEntryMetadata(value?.metadata, bareOs)
return personalDrive.put(physPath, buf, { executable, metadata })
}
/**
* Ensure `/.bare` and `/.bare/holesail` DIR_MARKER entries exist on the personal drive for stable
* Holesail paths (needed when `/` is skipped as a virtual mount point in parent checks, and when
* `/.bare` is not the same backing path as `/.bare/holesail/**` under account prefix).
*/
async function ensureBareHolesailStablePersonalDirTree(physPath, logicalAbs) {
if (!personalDrive) return
const norm = String(physPath || '').replace(/\/+$/, '') || '/'
const d = dirnameAbs(norm)
if (d === '/' || d === '') return
const physPrefs = pathPrefixes(d)
const empty = new Uint8Array(0)
for (let i = 1; i < physPrefs.length; i++) {
const physPre = physPrefs[i]
const st = await lstatPersonalDrivePhysicalDir(physPre)
if (st) continue
const markerPath =
physPre === '/' ? `/${DIR_MARKER}` : `${physPre}/${DIR_MARKER}`
await putPersonalDrivePhysical(logicalAbs, markerPath, empty, {})
}
}
return {
lstatPersonalDrivePhysicalDir,
lstatPersonalDrivePhysicalAny,
assertPersonalDriveAncestorWritableForCreate,
putPersonalDrivePhysical,
ensureBareHolesailStablePersonalDirTree
}
}
+182 -3
View File
@@ -2,6 +2,7 @@
* VFS mount / mirror / tilde / system-RO alias routers used by createVfs.
*/
import unixPathResolve from 'unix-path-resolve'
import { classifyBareOsVfsPseudoAbs } from './vfs-pseudo-classify.js'
/**
* @param {{
@@ -11,7 +12,18 @@ import unixPathResolve from 'unix-path-resolve'
* systemRoAliasNorm: string,
* getAuxiliaryMountLines?: (() => (string | null | undefined)[]) | null,
* getAuxiliaryDrives?: (() => unknown[] | null | undefined) | null,
* systemDrive: unknown
* systemDrive: unknown,
* personalDrive?: unknown,
* env?: Record<string, string | undefined>,
* tmpStorageRoot?: () => string,
* varLogStorageRoot?: () => string,
* activeHomeBasename?: () => string,
* personalHomeStorageRoot?: () => string,
* usePersonalAcctPrefix?: () => boolean,
* personalLayoutRootAbs?: () => string,
* omitHypercorePackHrpcLifecycleProc?: boolean,
* bareOsIpc?: unknown,
* ipcFifoLogicalToActual?: (logicalName: string) => string
* }} deps
*/
export function createVfsRouteHelpers(deps) {
@@ -22,7 +34,18 @@ export function createVfsRouteHelpers(deps) {
systemRoAliasNorm,
getAuxiliaryMountLines = null,
getAuxiliaryDrives = null,
systemDrive
systemDrive,
personalDrive = null,
env = {},
tmpStorageRoot = () => '/.bare-os/tmp',
varLogStorageRoot = () => '/.bare-os/var/log',
activeHomeBasename = () => '',
personalHomeStorageRoot = () => '/',
usePersonalAcctPrefix = () => false,
personalLayoutRootAbs = () => '/',
omitHypercorePackHrpcLifecycleProc = false,
bareOsIpc = null,
ipcFifoLogicalToActual = null
} = deps
/**
@@ -192,6 +215,160 @@ export function createVfsRouteHelpers(deps) {
}
}
/**
* @param {string} absPath
* @returns {null | Record<string, unknown>}
*/
function classifyPseudoAbs(absPath) {
return classifyBareOsVfsPseudoAbs(absPath, {
omitHypercorePackHrpcLifecycleProc,
bareOsIpc,
ipcFifoLogicalToActual
})
}
function route(absPath) {
const aliasR = routeSystemRoAlias(absPath)
if (aliasR) return aliasR
const mirR = routeMirror(absPath)
if (mirR) return mirR
const mntR = routeMnt(absPath)
if (mntR) return mntR
const pseudo = classifyPseudoAbs(absPath)
if (pseudo) return pseudo
const tmpRoot = tmpStorageRoot()
const tmpNorm = absPath.replace(/\/+$/, '') || '/'
if (tmpNorm === '/tmp') {
return { drive: personalDrive, path: tmpRoot }
}
if (absPath.startsWith('/tmp/')) {
const rel = absPath.slice(5).replace(/^\/+/, '')
const p = rel ? unixPathResolve(tmpRoot, rel) : tmpRoot
return { drive: personalDrive, path: p }
}
const varNorm = absPath.replace(/\/+$/, '') || '/'
if (varNorm === '/var') {
return { virtualVarRoot: true }
}
const varLogRoot = varLogStorageRoot()
if (varNorm === '/var/log' || absPath === '/var/log/') {
return { drive: personalDrive, path: varLogRoot }
}
if (absPath.startsWith('/var/log/')) {
const rel = absPath.slice('/var/log/'.length).replace(/^\/+/, '')
const p = rel ? unixPathResolve(varLogRoot, rel) : varLogRoot
return { drive: personalDrive, path: p }
}
if (absPath.startsWith('/var/')) {
return { drive: systemDrive, path: absPath }
}
const h = normalizeHome()
const activeSeg = activeHomeBasename()
const homeRoot = personalHomeStorageRoot()
if (activeSeg && absPath === '/home') {
return { virtualHomeDir: true }
}
if (activeSeg && absPath.startsWith('/home/')) {
const after = absPath.slice('/home/'.length)
const slash = after.indexOf('/')
const seg = slash === -1 ? after : after.slice(0, slash)
const rest = slash === -1 ? '' : after.slice(slash + 1)
if (seg === activeSeg) {
if (!rest) {
return { drive: personalDrive, path: homeRoot }
}
const rel = rest.replace(/^\/+/, '')
const wwwR = routeHomeWwwAlias(rel)
if (wwwR) return wwwR
const p = unixPathResolve(homeRoot, rel)
return { drive: personalDrive, path: p }
}
return { drive: systemDrive, path: absPath }
}
if (absPath === h || absPath.startsWith(h + '/')) {
if (absPath === h) {
return { drive: personalDrive, path: homeRoot }
}
const rel = absPath.slice(h.length + 1).replace(/^\/+/, '')
const wwwR = routeHomeWwwAlias(rel)
if (wwwR) return wwwR
const p = unixPathResolve(homeRoot, rel)
return { drive: personalDrive, path: p }
}
const unionNorm = absPath.replace(/\/+$/, '') || '/'
if (
unionNorm === '/.bare-os/union' ||
absPath.startsWith('/.bare-os/union/')
) {
const p = unionNorm === '/.bare-os/union' ? '/.bare-os/union' : unionNorm
return { drive: personalDrive, path: p }
}
const snapsOn =
env &&
(env.BARE_OS_VFS_SNAPSHOTS === '1' || env.BARE_OS_VFS_SNAPSHOTS === 'true')
if (
snapsOn &&
systemDrive &&
typeof systemDrive.checkout === 'function'
) {
const snapNorm = absPath.replace(/\/+$/, '') || '/'
if (snapNorm === '/snapshots') {
return { virtualSnapshotRoot: true }
}
if (snapNorm === '/snapshots/system') {
return { virtualSnapshotSystem: true }
}
const m = /^\/snapshots\/system\/([0-9]+)(\/.*)?$/.exec(absPath)
if (m) {
const ver = Number.parseInt(m[1], 10)
const tail = m[2] || '/'
const sub =
tail === '/' ? '/' : unixPathResolve('/', tail.replace(/^\/+/, ''))
try {
const chk = systemDrive.checkout(ver)
if (chk)
return { drive: chk, path: sub, snapshotReadOnly: true }
} catch {
/* fall through to system path */
}
}
}
const bareTop = absPath.replace(/\/+$/, '') || '/'
if (
personalDrive &&
(bareTop === '/.bare' || absPath.startsWith('/.bare/'))
) {
const p =
bareTop === '/.bare'
? '/.bare'
: absPath.replace(/\/+$/, '') || absPath
if (usePersonalAcctPrefix()) {
const tail = bareTop === '/.bare' ? '' : p.slice('/.bare'.length)
// Managed Holesail persistence: keep `/.bare/holesail/**` on the personal drive root so
// guest vs unlocked sessions share one `state.json` (not under per-session `acct/…` layout).
if (tail === '/holesail' || tail.startsWith('/holesail/')) {
return { drive: personalDrive, path: p }
}
const layoutRoot = personalLayoutRootAbs()
const physical = layoutRoot + '/.bare' + tail
return { drive: personalDrive, path: physical }
}
return { drive: personalDrive, path: p }
}
return { drive: systemDrive, path: absPath }
}
return {
expandTilde,
resolveLogical,
@@ -200,6 +377,8 @@ export function createVfsRouteHelpers(deps) {
routeMirror,
routeMnt,
routeSystemRoAlias,
routeHomeWwwAlias
routeHomeWwwAlias,
classifyPseudoAbs,
route
}
}
+32 -315
View File
@@ -33,6 +33,7 @@ import {
} from './vfs-policy.js'
import { createVfsWarmReadCache } from './vfs-warm-cache.js'
import { createVfsRouteHelpers } from './vfs-route.js'
import { createVfsPersonalDriveIo } from './vfs-personal-io.js'
import {
BARE_OS_PROC_FILE_TO_ID_REPLICATION_OPERATOR_SURFACE,
BARE_OS_PROC_FILE_TO_ID_PEAR_CORESTORE_HRPC,
@@ -41,7 +42,6 @@ import {
BARE_OS_PROC_FILE_TO_ID_PEAR_INSPECT_LOGGER_TLS,
BARE_OS_PROC_FILE_TO_ID_HYPERCORE_PACK_HRPC_LIFECYCLE
} from './vfs-proc-id-maps.js'
import { classifyBareOsVfsPseudoAbs } from './vfs-pseudo-classify.js'
import {
isHyperdriveRootPath,
joinLogical,
@@ -630,10 +630,8 @@ export function createVfs(
resolveLogical,
getMntMap,
pseudoMountsText,
routeMirror,
routeMnt,
routeSystemRoAlias,
routeHomeWwwAlias
classifyPseudoAbs,
route
} = createVfsRouteHelpers({
getCwd: () => cwd,
normalizeHome,
@@ -641,7 +639,18 @@ export function createVfs(
systemRoAliasNorm,
getAuxiliaryMountLines,
getAuxiliaryDrives,
systemDrive
systemDrive,
personalDrive,
env,
tmpStorageRoot,
varLogStorageRoot,
activeHomeBasename,
personalHomeStorageRoot,
usePersonalAcctPrefix,
personalLayoutRootAbs,
omitHypercorePackHrpcLifecycleProc,
bareOsIpc,
ipcFifoLogicalToActual
})
/**
@@ -1758,18 +1767,6 @@ export function createVfs(
return utf8Encode('')
}
/**
* @param {string} absPath
* @returns {null | Record<string, unknown>}
*/
function classifyPseudoAbs(absPath) {
return classifyBareOsVfsPseudoAbs(absPath, {
omitHypercorePackHrpcLifecycleProc,
bareOsIpc,
ipcFifoLogicalToActual
})
}
function lstatVirtualPseudo(abs, r) {
if (!r.virtualPseudo) return null
if (r.node === 'enoent') return null
@@ -1792,148 +1789,6 @@ export function createVfs(
return st
}
function route(absPath) {
const aliasR = routeSystemRoAlias(absPath)
if (aliasR) return aliasR
const mirR = routeMirror(absPath)
if (mirR) return mirR
const mntR = routeMnt(absPath)
if (mntR) return mntR
const pseudo = classifyPseudoAbs(absPath)
if (pseudo) return pseudo
const tmpRoot = tmpStorageRoot()
const tmpNorm = absPath.replace(/\/+$/, '') || '/'
if (tmpNorm === '/tmp') {
return { drive: personalDrive, path: tmpRoot }
}
if (absPath.startsWith('/tmp/')) {
const rel = absPath.slice(5).replace(/^\/+/, '')
const p = rel ? unixPathResolve(tmpRoot, rel) : tmpRoot
return { drive: personalDrive, path: p }
}
const varNorm = absPath.replace(/\/+$/, '') || '/'
if (varNorm === '/var') {
return { virtualVarRoot: true }
}
const varLogRoot = varLogStorageRoot()
if (varNorm === '/var/log' || absPath === '/var/log/') {
return { drive: personalDrive, path: varLogRoot }
}
if (absPath.startsWith('/var/log/')) {
const rel = absPath.slice('/var/log/'.length).replace(/^\/+/, '')
const p = rel ? unixPathResolve(varLogRoot, rel) : varLogRoot
return { drive: personalDrive, path: p }
}
if (absPath.startsWith('/var/')) {
return { drive: systemDrive, path: absPath }
}
const h = normalizeHome()
const activeSeg = activeHomeBasename()
const homeRoot = personalHomeStorageRoot()
if (activeSeg && absPath === '/home') {
return { virtualHomeDir: true }
}
if (activeSeg && absPath.startsWith('/home/')) {
const after = absPath.slice('/home/'.length)
const slash = after.indexOf('/')
const seg = slash === -1 ? after : after.slice(0, slash)
const rest = slash === -1 ? '' : after.slice(slash + 1)
if (seg === activeSeg) {
if (!rest) {
return { drive: personalDrive, path: homeRoot }
}
const rel = rest.replace(/^\/+/, '')
const wwwR = routeHomeWwwAlias(rel)
if (wwwR) return wwwR
const p = unixPathResolve(homeRoot, rel)
return { drive: personalDrive, path: p }
}
return { drive: systemDrive, path: absPath }
}
if (absPath === h || absPath.startsWith(h + '/')) {
if (absPath === h) {
return { drive: personalDrive, path: homeRoot }
}
const rel = absPath.slice(h.length + 1).replace(/^\/+/, '')
const wwwR = routeHomeWwwAlias(rel)
if (wwwR) return wwwR
const p = unixPathResolve(homeRoot, rel)
return { drive: personalDrive, path: p }
}
const unionNorm = absPath.replace(/\/+$/, '') || '/'
if (
unionNorm === '/.bare-os/union' ||
absPath.startsWith('/.bare-os/union/')
) {
const p = unionNorm === '/.bare-os/union' ? '/.bare-os/union' : unionNorm
return { drive: personalDrive, path: p }
}
const snapsOn =
env &&
(env.BARE_OS_VFS_SNAPSHOTS === '1' || env.BARE_OS_VFS_SNAPSHOTS === 'true')
if (
snapsOn &&
systemDrive &&
typeof systemDrive.checkout === 'function'
) {
const snapNorm = absPath.replace(/\/+$/, '') || '/'
if (snapNorm === '/snapshots') {
return { virtualSnapshotRoot: true }
}
if (snapNorm === '/snapshots/system') {
return { virtualSnapshotSystem: true }
}
const m = /^\/snapshots\/system\/([0-9]+)(\/.*)?$/.exec(absPath)
if (m) {
const ver = Number.parseInt(m[1], 10)
const tail = m[2] || '/'
const sub =
tail === '/' ? '/' : unixPathResolve('/', tail.replace(/^\/+/, ''))
try {
const chk = systemDrive.checkout(ver)
if (chk)
return { drive: chk, path: sub, snapshotReadOnly: true }
} catch {
/* fall through to system path */
}
}
}
const bareTop = absPath.replace(/\/+$/, '') || '/'
if (
personalDrive &&
(bareTop === '/.bare' || absPath.startsWith('/.bare/'))
) {
const p =
bareTop === '/.bare'
? '/.bare'
: absPath.replace(/\/+$/, '') || absPath
if (usePersonalAcctPrefix()) {
const tail = bareTop === '/.bare' ? '' : p.slice('/.bare'.length)
// Managed Holesail persistence: keep `/.bare/holesail/**` on the personal drive root so
// guest vs unlocked sessions share one `state.json` (not under per-session `acct/…` layout).
if (tail === '/holesail' || tail.startsWith('/holesail/')) {
return { drive: personalDrive, path: p }
}
const layoutRoot = personalLayoutRootAbs()
const physical = layoutRoot + '/.bare' + tail
return { drive: personalDrive, path: physical }
}
return { drive: personalDrive, path: p }
}
return { drive: systemDrive, path: absPath }
}
function personalVaultReaddirCacheTtlMs() {
const raw = env && env.BARE_OS_PERSONAL_VAULT_INDEX_CACHE_MS
if (raw == null || raw === '' || raw === '0' || raw === 'false') return 0
@@ -2002,6 +1857,23 @@ export function createVfs(
return null
}
const {
lstatPersonalDrivePhysicalDir,
lstatPersonalDrivePhysicalAny,
assertPersonalDriveAncestorWritableForCreate,
putPersonalDrivePhysical,
ensureBareHolesailStablePersonalDirTree
} = createVfsPersonalDriveIo({
personalDrive,
env,
DIR_MARKER,
entryOn,
statFromEntryValue,
assertNotBootPolicyDenyVfs,
assertUnionWriteNotDenied,
assertGuestSensitivePersonalOp
})
async function lstatFromAbs(abs) {
if (abs === '/mirror' || abs === '/mirror/') {
return { ...synthesizeStat(abs, false, env, 'directory'), path: abs }
@@ -2330,161 +2202,6 @@ export function createVfs(
}
}
/**
* `/.bare/holesail/**` on the personal drive: parent checks and traverse must walk **physical**
* paths on that drive — logical `/.bare` may map to a different backing path under
* {@link usePersonalAcctPrefix}, and `/` is skipped as a virtual mount point.
*/
async function lstatPersonalDrivePhysicalDir(physPath) {
if (!personalDrive) return null
const pnorm = String(physPath || '').replace(/\/+$/, '') || '/'
if (pnorm === '/') {
return { ...synthesizeStat('/', true, env, 'directory'), path: pnorm }
}
const e = await entryOn(personalDrive, pnorm, { follow: false })
const fromVal = statFromEntryValue(
pnorm,
{ drive: personalDrive, path: pnorm },
true,
e
)
if (fromVal) return { ...fromVal, path: pnorm }
const names = []
try {
const stream = personalDrive.readdir(pnorm)
for await (const n of stream) names.push(n)
} catch {
/* missing parent */
}
if (names.length) {
if (names.includes(DIR_MARKER)) {
const mPath = `${pnorm}/${DIR_MARKER}`.replace(/\/{2,}/g, '/')
const me = await entryOn(personalDrive, mPath, { follow: false })
const mv = me?.value
if (mv?.blob) {
const bo = extractBareOs(mv)
if (bo) {
let perm = bo.mode & 0o777
if (perm & 0o400) perm |= 0o100
if (perm & 0o040) perm |= 0o010
if (perm & 0o004) perm |= 0o001
return statFromBareOs(
{ ...bo, mode: S_IFDIR | perm },
'directory',
0,
pnorm
)
}
}
}
return { ...synthesizeStat(pnorm, true, env, 'directory'), path: pnorm }
}
if (e) return { ...synthesizeStat(pnorm, true, env, 'directory'), path: pnorm }
return null
}
/** Directory or regular file (or symlink) on the personal drive by absolute path on that drive. */
async function lstatPersonalDrivePhysicalAny(physPath) {
if (!personalDrive) return null
const pnorm = String(physPath || '').replace(/\/+$/, '') || '/'
if (pnorm === '/') {
return { ...synthesizeStat('/', true, env, 'directory'), path: pnorm }
}
const e = await entryOn(personalDrive, pnorm, { follow: false })
const rf = statFromEntryValue(
pnorm,
{ drive: personalDrive, path: pnorm },
true,
e
)
if (rf) return rf
return await lstatPersonalDrivePhysicalDir(pnorm)
}
async function assertPersonalDriveAncestorWritableForCreate(physPath) {
if (!personalDrive) {
throw new Error('ENOENT: ' + String(physPath || ''))
}
const { uid: euid, gid: egid } = parseUidGid(env)
const norm = String(physPath || '').replace(/\/+$/, '') || '/'
let probe = dirnameAbs(norm)
/** @type {{ pre: string, st: Awaited<ReturnType<typeof lstatFromAbs>> } | null} */
let deepest = null
while (probe && probe !== '/') {
const st = await lstatPersonalDrivePhysicalDir(probe)
if (st && st.type === 'directory') {
deepest = { pre: probe, st }
break
}
probe = dirnameAbs(probe)
}
if (!deepest) {
deepest = {
pre: '/',
st: await lstatPersonalDrivePhysicalDir('/')
}
}
if (!deepest.st || !modeAllows(deepest.st, euid, egid, 'w', 'directory')) {
throw new Error('EACCES: cannot create in ' + String(deepest?.pre ?? ''))
}
}
/**
* @param {string} logicalAbs policy / guest messaging
* @param {string} physPath path on {@link personalDrive}
*/
async function putPersonalDrivePhysical(logicalAbs, physPath, buf, opts = {}) {
assertNotBootPolicyDenyVfs(logicalAbs, 'write')
assertUnionWriteNotDenied(logicalAbs)
if (!personalDrive) {
throw new Error('Read-only path (not under $HOME): ' + logicalAbs)
}
if (await bareOsVfsAclDeniesDriveOp(env, personalDrive, physPath, 'write')) {
throw new Error('EACCES: ACL enforces deny write: ' + logicalAbs)
}
assertGuestSensitivePersonalOp(personalDrive, physPath, 'write', logicalAbs)
const existing = await entryOn(personalDrive, physPath, { follow: false })
const hadBlob = !!existing?.value?.blob
if (!hadBlob) await assertPersonalDriveAncestorWritableForCreate(physPath)
const value = existing?.value
const prevBare = extractBareOs(value)
const bareOs = mergeBareOsOnWrite(prevBare, env, {
executable: opts.executable,
bumpMtime: opts.bumpMtime !== false,
touchCtime: opts.touchCtime === true,
legacyExecutable: !!value?.executable,
mtimeMs: opts.mtimeMs,
ctimeMs: opts.ctimeMs,
posixModeBits: opts.posixModeBits
})
const executable =
opts.executable !== undefined ? !!opts.executable : !!value?.executable
const metadata = mergeEntryMetadata(value?.metadata, bareOs)
return personalDrive.put(physPath, buf, { executable, metadata })
}
/**
* Ensure `/.bare` and `/.bare/holesail` DIR_MARKER entries exist on the personal drive for stable
* Holesail paths (needed when `/` is skipped as a virtual mount point in parent checks, and when
* `/.bare` is not the same backing path as `/.bare/holesail/**` under {@link usePersonalAcctPrefix}).
*/
async function ensureBareHolesailStablePersonalDirTree(physPath, logicalAbs) {
if (!personalDrive) return
const norm = String(physPath || '').replace(/\/+$/, '') || '/'
const d = dirnameAbs(norm)
if (d === '/' || d === '') return
const physPrefs = pathPrefixes(d)
const empty = new Uint8Array(0)
for (let i = 1; i < physPrefs.length; i++) {
const physPre = physPrefs[i]
const st = await lstatPersonalDrivePhysicalDir(physPre)
if (st) continue
const markerPath =
physPre === '/' ? `/${DIR_MARKER}` : `${physPre}/${DIR_MARKER}`
await putPersonalDrivePhysical(logicalAbs, markerPath, empty, {})
}
}
async function assertParentWritableForCreate(abs) {
const parent = dirnameAbs(abs)
if (parent === abs) return
+77 -1
View File
@@ -82,10 +82,13 @@ import {
tryReportMisplacedReservedStatementStart,
casePatternList,
execDoubleBracketLimited,
bareOsResetShellIdentityState
bareOsResetShellIdentityState,
consumeInteractiveHeredoc
} from './lib/shell-stmt.js'
import { createVfsRouteHelpers } from './lib/vfs-route.js'
import { createBareOsPosixFdSimMethods } from './lib/bare-os-posix-fd-sim.js'
import { createBareOsPathconfMethods } from './lib/bare-os-pathconf.js'
import { createBareOsLogicalFdMethods } from './lib/bare-os-logical-fd.js'
test('posix env flags and caps', (t) => {
t.ok(wantPosixSocketFdBridge({ BARE_OS_POSIX_SOCKET_FD_BRIDGE: '1' }))
@@ -553,6 +556,79 @@ test('vfs route tilde mnt and ro alias', (t) => {
t.ok(helpers.pseudoMountsText().includes('/mnt/data'))
})
test('vfs route tmp home union snapshots', (t) => {
const personal = { id: 'p' }
const system = { id: 'sys', checkout: (v) => ({ id: 'chk' + v }) }
const helpers = createVfsRouteHelpers({
getCwd: () => '/home/g',
normalizeHome: () => '/home/g',
systemRoAliasNorm: '',
systemDrive: system,
personalDrive: personal,
env: { BARE_OS_VFS_SNAPSHOTS: '1' },
tmpStorageRoot: () => '/.bare-os/tmp/g',
varLogStorageRoot: () => '/.bare-os/var/log/g',
activeHomeBasename: () => 'g',
personalHomeStorageRoot: () => '/.bare-os/home/g',
usePersonalAcctPrefix: () => false
})
t.is(helpers.route('/tmp/x').path, '/.bare-os/tmp/g/x')
t.ok(helpers.route('/var').virtualVarRoot)
t.is(helpers.route('/home/g/a').drive, personal)
t.is(helpers.route('/snapshots/system/3/etc').snapshotReadOnly, true)
t.is(helpers.route('/.bare/holesail/state.json').path, '/.bare/holesail/state.json')
})
test('pathconf and getconf helpers', (t) => {
const m = createBareOsPathconfMethods({
env: { BARE_OS_NPROC: '8', BARE_OS_PIPELINE_MAX_STAGES: '12' },
resolveLogical: (p) => p,
bootHrtimeNowNs: null
})
t.is(m.bareOsPathconf('/etc', 'NAME_MAX'), 255)
t.is(m.bareOsPathconf('/mirror/x', '_PC_CHOWN_RESTRICTED'), 0)
t.is(m.bareOsPathconf('/dev/shm/a', '_PC_NAME_MAX'), 128)
t.is(m.bareOsGetconfSysconf('_SC_NPROCESSORS_ONLN'), '8')
t.is(m.bareOsGetconfSysconf('_SC_PAGESIZE'), '4096')
t.is(m.bareOsGetconfSysconf('_SC_BARE_OS_PIPELINE_MAX_STAGES'), '12')
t.is(m.bareOsGetconfSysconf('_SC_MONOTONIC_CLOCK_RES'), '0')
})
test('logical fd register and sigaction', (t) => {
const ctx = {
bareOsLogicalFds: {},
bareOsLogicalFdFlags: { 9: 1 },
bareOsLogicalSigaction: {},
bareOsSnapshotHandles: [],
...createBareOsLogicalFdMethods()
}
ctx.bareOsRegisterLogicalFd(9, '/tmp/x')
t.is(ctx.bareOsLogicalFds['9'], '/tmp/x')
ctx.bareOsUnregisterLogicalFd(9)
t.is(ctx.bareOsLogicalFds['9'], undefined)
t.is(ctx.bareOsSigaction('INT', 'IGNORE').mode, 'IGNORE')
t.is(ctx.bareOsLogicalSigaction.INT, 'IGNORE')
ctx.bareOsRegisterSnapshotHandle({ id: 's1' })
t.is(ctx.bareOsSnapshotHandles[0].id, 's1')
})
test('consumeInteractiveHeredoc reads until delim', async (t) => {
const lines = ['one', 'two', 'END']
const ctx = {
readLine: async () => lines.shift(),
vfs: { env: {} },
console: { error: () => {} }
}
const r = await consumeInteractiveHeredoc(
ctx,
"cat <<END",
() => {}
)
t.ok(r.ok)
t.is(r.execLine, 'cat')
t.is(ctx.shellHeredocOnce, 'one\ntwo\n')
})
test('posix fd sim pipe write read', (t) => {
const methods = createBareOsPosixFdSimMethods({
env: { BARE_OS_POSIX_FD_SIM: '1' },