723 lines
23 KiB
JavaScript
723 lines
23 KiB
JavaScript
import test from 'brittle'
|
|
import {
|
|
wantPosixSocketFdBridge,
|
|
wantPosixFcntlBlockingWait,
|
|
bareOsPosixDgramRecvQueueMax,
|
|
bareOsPosixAcceptQueueMax,
|
|
bareOsFlattenSendmsgIovs,
|
|
bareOsSendmsgAncillaryRejectReason,
|
|
bareOsResolveCooperativeLockPath
|
|
} from './lib/posix/bare-os-posix-syscall-helpers.js'
|
|
import { redactAuditShellLine } from './lib/security/bare-os-audit-redact.js'
|
|
import {
|
|
isExecLineBuiltinDenied,
|
|
shellCommandDeniedByPolicy,
|
|
shellUnsafeRedirectPath
|
|
} from './lib/shell/shell-policy.js'
|
|
import {
|
|
joinLogical,
|
|
dirnameAbs,
|
|
pathPrefixes,
|
|
isHyperdriveRootPath
|
|
} from './lib/vfs/vfs-path.js'
|
|
import { bareOsProtomuxTuningFromEnv } from './lib/p2p/swarm-opts.js'
|
|
import {
|
|
bareOsDupLogicalFdForScm,
|
|
prepareBridgeSendmsg,
|
|
bareOsShutdownSocketBridgeFd
|
|
} from './lib/posix/bare-os-socket-bridge.js'
|
|
import { createStockBareOsHrpcRequest } from './lib/tools/bare-os-hrpc-stock-request.js'
|
|
import {
|
|
splitTopLevelStatements,
|
|
isValidShellIdentifier,
|
|
parseShellFunctionDeclaration,
|
|
casePatternMatches
|
|
} from './lib/shell/shell-syntax.js'
|
|
import { createVfsPersonalLayout } from './lib/vfs/vfs-personal.js'
|
|
import {
|
|
listBareOsShellBuiltins,
|
|
isShellBuiltin,
|
|
bareOsShellReadSplitFields
|
|
} from './lib/shell/shell-builtins.js'
|
|
import {
|
|
getBareOsPipelineLimits,
|
|
mergePipelineChildCtx,
|
|
DEFAULT_PIPELINE_MAX_STAGES
|
|
} from './lib/shell/shell-runtime.js'
|
|
import { expandWord, expandShellWordTokens } from './lib/shell/shell-expand.js'
|
|
import {
|
|
utf8Encode,
|
|
createVfsPseudoLinux
|
|
} from './lib/vfs/vfs-pseudo-linux.js'
|
|
import { createBareOsVirtualSignalDeliverer } from './lib/posix/bare-os-virtual-signal.js'
|
|
import { createKernelLoaderAuditAppend } from './lib/host/bare-os-loader-audit.js'
|
|
import { createBooterBootEmitter } from './lib/boot/bare-os-boot-phases.js'
|
|
import { createBooterProcHelpers } from './lib/proc/bare-os-booter-proc-helpers.js'
|
|
import { tokenize } from './lib/shell/shell-token.js'
|
|
import {
|
|
defaultShellAliases,
|
|
expandArgvAliases,
|
|
applyAliasDefinition
|
|
} from './lib/shell/shell-alias.js'
|
|
import {
|
|
parsePipeline,
|
|
splitTokensBySemicolon,
|
|
splitTokensByAndOr,
|
|
planShellRedirections,
|
|
buildShellExecutionGraph
|
|
} from './lib/shell/shell-parse.js'
|
|
import {
|
|
assertUnionWriteNotDenied,
|
|
assertNotBootPolicyDenyVfs
|
|
} from './lib/vfs/vfs-policy.js'
|
|
import { createVfsWarmReadCache } from './lib/vfs/vfs-warm-cache.js'
|
|
import { createSessionReadMaskedLine } from './lib/cli/cli-readline.js'
|
|
import {
|
|
bareOsPipelineChildCtx,
|
|
bareOsPipelineRawChunkToText,
|
|
runWithShellPipelineStageTimeout
|
|
} from './lib/shell/shell-pipeline.js'
|
|
import {
|
|
assignShellFunctionPositionalEnv,
|
|
tryReportMisplacedReservedStatementStart,
|
|
casePatternList,
|
|
execDoubleBracketLimited,
|
|
bareOsResetShellIdentityState,
|
|
consumeInteractiveHeredoc
|
|
} from './lib/shell/shell-stmt.js'
|
|
import { createVfsRouteHelpers } from './lib/vfs/vfs-route.js'
|
|
import { createBareOsPosixFdSimMethods } from './lib/posix/bare-os-posix-fd-sim.js'
|
|
import { createBareOsPathconfMethods } from './lib/posix/bare-os-pathconf.js'
|
|
import { createBareOsLogicalFdMethods } from './lib/posix/bare-os-logical-fd.js'
|
|
import { createVfsPseudoFileBytes } from './lib/vfs/vfs-pseudo-bytes.js'
|
|
import { createBareOsLifecycleHookMethods } from './lib/host/bare-os-lifecycle-hooks.js'
|
|
import { createBareOsProcessControlMethods } from './lib/posix/bare-os-process-control.js'
|
|
|
|
test('posix env flags and caps', (t) => {
|
|
t.ok(wantPosixSocketFdBridge({ BARE_OS_POSIX_SOCKET_FD_BRIDGE: '1' }))
|
|
t.ok(!wantPosixSocketFdBridge({}))
|
|
t.ok(wantPosixFcntlBlockingWait({ BARE_OS_POSIX_FCNTL_BLOCKING_WAIT: 'yes' }))
|
|
t.is(bareOsPosixDgramRecvQueueMax({}), 256)
|
|
t.is(bareOsPosixDgramRecvQueueMax({ BARE_OS_POSIX_DGRAM_RECVQ_MAX: '8' }), 8)
|
|
t.is(bareOsPosixAcceptQueueMax({}), 64)
|
|
})
|
|
|
|
test('sendmsg flatten and ancillary reject', (t) => {
|
|
const a = bareOsFlattenSendmsgIovs({ iovs: [{ buf: new Uint8Array([1, 2]) }] })
|
|
t.ok(a.ok)
|
|
t.is(a.buf.byteLength, 2)
|
|
t.is(bareOsSendmsgAncillaryRejectReason({ cmsgs: [{}] }), 'cmsgs')
|
|
t.is(bareOsSendmsgAncillaryRejectReason({}), null)
|
|
t.is(
|
|
bareOsResolveCooperativeLockPath(
|
|
{ fd: 7 },
|
|
{ bareOsLogicalFds: { 7: '/tmp/x' } }
|
|
),
|
|
'/tmp/x'
|
|
)
|
|
})
|
|
|
|
test('audit redact and shell policy', (t) => {
|
|
t.is(redactAuditShellLine('PASSWORD=secret TOKEN=x', {}), 'PASSWORD=secret TOKEN=x')
|
|
t.is(
|
|
redactAuditShellLine('PASSWORD=secret TOKEN=x', { BARE_OS_AUDIT_REDACT: '1' }),
|
|
'PASSWORD=<redacted> TOKEN=<redacted>'
|
|
)
|
|
t.ok(isExecLineBuiltinDenied('cd', { BARE_OS_BOOT_POLICY_DENY_EXEC_LINE_BUILTINS: 'cd,export' }))
|
|
t.ok(!isExecLineBuiltinDenied('ls', { BARE_OS_BOOT_POLICY_DENY_EXEC_LINE_BUILTINS: 'cd' }))
|
|
t.ok(shellCommandDeniedByPolicy('rm', { BARE_OS_SHELL_DENY_COMMANDS: 'rm' }))
|
|
t.ok(
|
|
shellUnsafeRedirectPath('/proc/self/mem', { BARE_OS_SHELL_REDIRECT_GUARD: '1' })
|
|
)
|
|
})
|
|
|
|
test('vfs path helpers', (t) => {
|
|
t.ok(isHyperdriveRootPath('/'))
|
|
t.is(joinLogical('/', 'bin'), '/bin')
|
|
t.is(dirnameAbs('/home/user/x'), '/home/user')
|
|
t.alike(pathPrefixes('/a/b'), ['/', '/a', '/a/b'])
|
|
})
|
|
|
|
test('protomux tuning json', (t) => {
|
|
t.is(bareOsProtomuxTuningFromEnv({}), null)
|
|
t.alike(bareOsProtomuxTuningFromEnv({ BARE_OS_PROTOMUX_TUNING_JSON: '{' }), {
|
|
schema: 1,
|
|
parseError: true
|
|
})
|
|
t.is(
|
|
bareOsProtomuxTuningFromEnv({ BARE_OS_PROTOMUX_TUNING_JSON: '{"max":1}' }).max,
|
|
1
|
|
)
|
|
})
|
|
|
|
function mockBridgeCtx(extra = {}) {
|
|
const logical = { 8: '/tmp/x' }
|
|
const flags = { 8: 0x800 }
|
|
const sim = { nextFd: 20, nextPairId: 1, queues: Object.create(null), byteTotals: Object.create(null) }
|
|
const registered = []
|
|
return {
|
|
env: {},
|
|
bareOsLogicalFds: logical,
|
|
bareOsLogicalFdFlags: flags,
|
|
bareOsPosixFdSimState: sim,
|
|
bareOsSocketBridgeByFd: Object.create(null),
|
|
bareOsRegisterLogicalFd(fd, target) {
|
|
logical[String(fd)] = target
|
|
registered.push([fd, target])
|
|
},
|
|
bareOsUnregisterLogicalFd() {},
|
|
_registered: registered,
|
|
...extra
|
|
}
|
|
}
|
|
|
|
test('socket bridge logical dup and sendmsg payload', (t) => {
|
|
const c = mockBridgeCtx()
|
|
const n = bareOsDupLogicalFdForScm(c, 8)
|
|
t.is(n, 20)
|
|
t.is(c.bareOsLogicalFds['20'], '/tmp/x')
|
|
t.is(c.bareOsLogicalFdFlags['20'], 0x800)
|
|
const prep = prepareBridgeSendmsg(c, {
|
|
iovs: [{ buf: new Uint8Array([9, 8, 7]) }]
|
|
})
|
|
t.ok(prep.ok)
|
|
t.is(prep.payload.byteLength, 3)
|
|
t.alike(prep.scmRightsLocalDup, [])
|
|
})
|
|
|
|
test('prepareBridgeSendmsg rejects ancillary without SCM_RIGHTS', (t) => {
|
|
const c = mockBridgeCtx()
|
|
const prep = prepareBridgeSendmsg(c, {
|
|
iovs: [{ buf: new Uint8Array([1]) }],
|
|
cmsgs: [{ fds: [8] }]
|
|
})
|
|
t.absent(prep.ok)
|
|
t.is(prep.err.code, 'ENOTSUP')
|
|
})
|
|
|
|
test('bareOsShutdownSocketBridgeFd EINVAL on unknown fd', async (t) => {
|
|
const c = mockBridgeCtx()
|
|
const r = await bareOsShutdownSocketBridgeFd(c, 99)
|
|
t.absent(r.ok)
|
|
t.is(r.code, 'EINVAL')
|
|
})
|
|
|
|
test('stock hrpc kernel.ping and allowlist', async (t) => {
|
|
const audit = []
|
|
const hrpc = createStockBareOsHrpcRequest({
|
|
env: { BARE_OS_HRPC_AUDIT: '1' },
|
|
sessionId: 'sess1',
|
|
vfs: { readFile: async () => new Uint8Array() },
|
|
disk: {},
|
|
capabilityWords: { primary: 1 },
|
|
protocolVersion: '0.0-test',
|
|
booterVersion: '0.0-test',
|
|
auditAppend: (row) => audit.push(row)
|
|
})
|
|
t.ok(hrpc.__bareOsStockHrpcRequest)
|
|
const ping = await hrpc('kernel', 'ping')
|
|
t.ok(ping.ok)
|
|
t.is(ping.sessionId, 'sess1')
|
|
t.is(audit[0].route, 'kernel.ping')
|
|
await hrpc('kernel', 'capabilities').then((r) => {
|
|
t.ok(r.ok)
|
|
t.ok(r.hrpcStockRoutes.includes('kernel.ping'))
|
|
})
|
|
const denied = createStockBareOsHrpcRequest({
|
|
env: { BARE_OS_HRPC_ALLOWLIST_JSON: '["kernel.ping"]' },
|
|
sessionId: 's',
|
|
vfs: {},
|
|
disk: {},
|
|
capabilityWords: {},
|
|
protocolVersion: 'x',
|
|
booterVersion: 'x'
|
|
})
|
|
await t.exception(denied('vfs', 'readText'), /allowlist/)
|
|
await t.exception(hrpc('nope', 'x'), /unsupported route/)
|
|
})
|
|
|
|
test('stock hrpc vfs.readText and echo', async (t) => {
|
|
const hrpc = createStockBareOsHrpcRequest({
|
|
env: {},
|
|
sessionId: 's',
|
|
vfs: {
|
|
async readFile(p) {
|
|
t.is(p, '/etc/os-release')
|
|
return new TextEncoder().encode('ID=bare')
|
|
}
|
|
},
|
|
disk: {},
|
|
capabilityWords: {},
|
|
protocolVersion: 'x',
|
|
booterVersion: 'x'
|
|
})
|
|
const txt = await hrpc('vfs', 'readText', { path: '/etc/os-release' })
|
|
t.is(txt.text, 'ID=bare')
|
|
const echo = await hrpc('bare_os', 'echo', { a: 1 })
|
|
t.alike(echo.payload, { a: 1 })
|
|
await t.exception(hrpc('vfs', 'readText', { path: 'relative' }), /absolute/)
|
|
})
|
|
|
|
test('shell syntax split and function parse', (t) => {
|
|
const toks = [
|
|
{ type: 'word', value: 'echo' },
|
|
{ type: 'word', value: 'a' },
|
|
{ type: 'op', value: ';' },
|
|
{ type: 'word', value: 'echo' },
|
|
{ type: 'word', value: 'b' }
|
|
]
|
|
const parts = splitTopLevelStatements(toks)
|
|
t.is(parts.length, 2)
|
|
t.ok(isValidShellIdentifier('foo_1'))
|
|
t.ok(!isValidShellIdentifier('1foo'))
|
|
const fn = parseShellFunctionDeclaration([
|
|
{ type: 'word', value: 'greet' },
|
|
{ type: 'op', value: '(' },
|
|
{ type: 'op', value: ')' },
|
|
{ type: 'op', value: '{' },
|
|
{ type: 'word', value: 'echo' },
|
|
{ type: 'word', value: 'hi' },
|
|
{ type: 'op', value: '}' }
|
|
])
|
|
t.is(fn.name, 'greet')
|
|
t.ok(casePatternMatches('abc', 'a*'))
|
|
t.ok(!casePatternMatches('abc', 'x*'))
|
|
})
|
|
|
|
test('vfs personal layout and guest-sensitive paths', (t) => {
|
|
const guest = createVfsPersonalLayout(
|
|
{ HOME: '/home/guest/', BARE_OS_PERSONAL_ACCT_PREFIX: '1' },
|
|
{ session: 'guest' }
|
|
)
|
|
t.is(guest.normalizeHome(), '/home/guest')
|
|
t.is(guest.activeHomeBasename(), 'guest')
|
|
t.is(guest.personalLayoutRootAbs(), '/.bare-os/acct/_guest')
|
|
t.is(guest.personalHomeStorageRoot(), '/.bare-os/acct/_guest/home/guest')
|
|
t.ok(guest.bareOsGuestSensitivePersonalDenied('/.bare/vault/key'))
|
|
t.ok(!guest.environKeyAllowed('PASSWORD'))
|
|
t.ok(guest.environKeyAllowed('HOME'))
|
|
const unlocked = createVfsPersonalLayout(
|
|
{ HOME: '/home/guest' },
|
|
{ session: 'unlocked' }
|
|
)
|
|
t.ok(!unlocked.bareOsGuestSensitivePersonalDenied('/.bare/vault/key'))
|
|
t.is(unlocked.personalLayoutRootAbs(), '/.bare-os')
|
|
})
|
|
|
|
test('shell builtins and read field split', (t) => {
|
|
t.ok(isShellBuiltin('cd', {}))
|
|
t.ok(!isShellBuiltin('read', {}))
|
|
t.ok(isShellBuiltin('read', { BARE_OS_SHELL_READ_BUILTIN: '1' }))
|
|
t.ok(listBareOsShellBuiltins({ BARE_OS_SHELL_READ_BUILTIN: '1' }).includes('read'))
|
|
t.alike(bareOsShellReadSplitFields('a b c', ' ', 2), ['a', 'b c'])
|
|
})
|
|
|
|
test('shell runtime pipeline limits and child merge', (t) => {
|
|
t.is(getBareOsPipelineLimits({}).maxStages, DEFAULT_PIPELINE_MAX_STAGES)
|
|
t.is(getBareOsPipelineLimits({ BARE_OS_PIPELINE_MAX_STAGES: '4' }).maxStages, 4)
|
|
const ctx = { exitCode: 0 }
|
|
mergePipelineChildCtx(ctx, {
|
|
exitCode: 7,
|
|
identity: { state: 'unlocked', publicKey: 1, secretKey: 2 }
|
|
})
|
|
t.is(ctx.exitCode, 7)
|
|
t.is(ctx.identity.state, 'unlocked')
|
|
})
|
|
|
|
test('expandWord param and arithmetic', (t) => {
|
|
t.is(expandWord('x${HOME}y', { HOME: '/h' }), 'x/hy')
|
|
t.is(expandWord('$(( $a + 1 ))', { a: '2' }), '3')
|
|
})
|
|
|
|
test('expandShellWordTokens traces and expands unquoted words', async (t) => {
|
|
const ctx = {}
|
|
const env = {
|
|
HOME: '/h',
|
|
BARE_OS_SHELL_EXPANSION_TRACE: '1',
|
|
BARE_OS_SHELL_CMDSUBST: '0'
|
|
}
|
|
const out = await expandShellWordTokens(
|
|
ctx,
|
|
{ type: 'word', value: '$HOME', parts: [{ q: 'u', t: '$HOME' }] },
|
|
env,
|
|
{ disablePathnameExpansion: true }
|
|
)
|
|
t.alike(out, ['/h'])
|
|
t.ok(Array.isArray(ctx.shellExpansionTrace))
|
|
t.ok(ctx.shellExpansionTrace.some((r) => r.stage === 'expand-pre'))
|
|
t.ok(ctx.shellExpansionTrace.some((r) => r.stage === 'split-glob'))
|
|
})
|
|
|
|
test('vfs linux-shaped /proc texts', (t) => {
|
|
const linux = createVfsPseudoLinux({
|
|
env: {
|
|
BARE_OS_CTX_API_VERSION: '1.2.3',
|
|
BARE_OS_SESSION_ID: 'sess-1',
|
|
HOME: '/home/guest',
|
|
PASSWORD: 'nope'
|
|
},
|
|
procSnapshot: { version: '1.2.3', cmdline: 'bare-os' },
|
|
bootStartedMs: Date.now() - 2500,
|
|
environKeyAllowed: (k) => k !== 'PASSWORD',
|
|
getProcSyntheticLinuxCompat: () => ({
|
|
sessionId: 'sess-1',
|
|
swarmPeerCount: 1,
|
|
peerIds: ['aabbccdd']
|
|
})
|
|
})
|
|
t.ok(linux.pseudoVersionText().includes('1.2.3'))
|
|
t.ok(linux.pseudoMeminfoText().includes('MemTotal:'))
|
|
t.ok(linux.pseudoCpuinfoText().includes('processor'))
|
|
t.ok(linux.pseudoNetTcpText().includes('ESTABLISHED'))
|
|
t.ok(linux.pseudoSelfCgroupsText().includes('sess-1'))
|
|
t.ok(utf8Encode('hi').byteLength >= 2)
|
|
})
|
|
|
|
test('virtual signal deliverer ignore and existence probe', (t) => {
|
|
const state = new Map()
|
|
const traps = []
|
|
const deliver = createBareOsVirtualSignalDeliverer({
|
|
dispatchShellTrapSignal: (ctx, sig) => {
|
|
traps.push(sig)
|
|
},
|
|
virtualSignalState: state
|
|
})
|
|
const ctx = {
|
|
bareOsLogicalSigaction: { TERM: 'IGNORE' },
|
|
requestBooterExit() {
|
|
t.fail('should not exit when ignored')
|
|
}
|
|
}
|
|
const ignored = deliver(ctx, 3, 'TERM')
|
|
t.ok(ignored.ignored)
|
|
t.is(traps.length, 0)
|
|
const probe = deliver({ requestBooterExit() {} }, 3, '0')
|
|
t.ok(probe.exists)
|
|
t.absent(probe.delivered)
|
|
})
|
|
|
|
test('loader audit appends when enabled', async (t) => {
|
|
const files = new Map()
|
|
const append = createKernelLoaderAuditAppend({
|
|
env: { BARE_OS_LOADER_AUDIT: '1' },
|
|
sessionId: 's1',
|
|
vfs: {
|
|
async readFile(p) {
|
|
if (!files.has(p)) throw new Error('missing')
|
|
return files.get(p)
|
|
},
|
|
async writeFile(p, buf) {
|
|
files.set(p, buf)
|
|
}
|
|
}
|
|
})
|
|
await append({ phase: 'vfs' })
|
|
const txt = new TextDecoder().decode(files.get('/run/bare-os/loader-audit.ndjson'))
|
|
t.ok(txt.includes('"type":"loader_audit"'))
|
|
t.ok(txt.includes('"phase":"vfs"'))
|
|
})
|
|
|
|
test('booter boot emitter records phases', (t) => {
|
|
const bootReadyStateRef = { booterPhases: [], booterStages: [] }
|
|
const events = []
|
|
const { emitBooterBootStep } = createBooterBootEmitter({
|
|
bootReadyStateRef,
|
|
bootStartedMs: Date.now(),
|
|
bootEventSubs: [(ev) => events.push(ev)],
|
|
diagnosticsSubs: [],
|
|
sessionId: 's',
|
|
lifecycleSchemaVersion: 1
|
|
})
|
|
emitBooterBootStep('vfs')
|
|
emitBooterBootStep('vfs')
|
|
t.alike(bootReadyStateRef.booterPhases, ['vfs'])
|
|
t.is(events.length, 2)
|
|
t.is(events[0].phase, 'booter:vfs')
|
|
})
|
|
|
|
test('booter proc helpers chat/meshdrop off notes', (t) => {
|
|
const helpers = createBooterProcHelpers({
|
|
interactiveCtxRef: { ctx: { bareOsLogicalFds: { 7: '/tmp/x' } } },
|
|
disk: {},
|
|
shellEnv: {},
|
|
hostEnv: {}
|
|
})
|
|
t.alike(helpers.bareOsCollectLogicalFdRows(), [{ fd: 7, target: '/tmp/x' }])
|
|
t.ok(helpers.bareOsChatProcSnapshotRecord().note)
|
|
t.ok(helpers.bareOsMeshdropProcSnapshotRecord().note)
|
|
})
|
|
|
|
test('shell token alias parse', (t) => {
|
|
const toks = tokenize('echo a; echo b')
|
|
t.ok(toks.length >= 3)
|
|
t.alike(expandArgvAliases(['ll'], defaultShellAliases()).slice(0, 1), ['ls'])
|
|
const ctx = { shellAliases: {} }
|
|
t.ok(applyAliasDefinition(ctx, "gst='git status'"))
|
|
t.is(ctx.shellAliases.gst, 'git status')
|
|
const pipe = parsePipeline(tokenize('echo hi > out'))
|
|
t.is(planShellRedirections(pipe[0]).stdout, 'truncate')
|
|
t.is(splitTokensBySemicolon(tokenize('a; b')).length, 2)
|
|
t.alike(splitTokensByAndOr(tokenize('a && b')).ops, ['&&'])
|
|
t.is(buildShellExecutionGraph('echo x').schema, 1)
|
|
})
|
|
|
|
test('vfs deny asserts and warm cache evict', (t) => {
|
|
t.exception(
|
|
() =>
|
|
assertUnionWriteNotDenied('/mnt/x/a', ['/mnt/x'], ['/mnt/x']),
|
|
/union write denied/
|
|
)
|
|
assertUnionWriteNotDenied('/home/g/a', ['/mnt/x'], ['/mnt/x'])
|
|
t.exception(
|
|
() =>
|
|
assertNotBootPolicyDenyVfs('/etc/shadow', 'read', {
|
|
BARE_OS_BOOT_POLICY_DENY_VFS: '/etc'
|
|
}),
|
|
/boot policy denies/
|
|
)
|
|
const ref = { current: null }
|
|
const cache = createVfsWarmReadCache({
|
|
env: { BARE_OS_VFS_BIN_CACHE: '1' },
|
|
warmReadCacheStatsRef: ref
|
|
})
|
|
t.ok(cache.binReadCache)
|
|
cache.binReadCache.set('/bin/ls', new Uint8Array([1]))
|
|
t.is(cache.bareOsEvictWarmReadPrefixes(['/bin/']), 1)
|
|
t.is(cache.binReadCache.size, 0)
|
|
})
|
|
|
|
test('session masked line falls back without TTY', async (t) => {
|
|
const read = createSessionReadMaskedLine({
|
|
stdin: {},
|
|
stdout: {},
|
|
fallbackReadLine: async (p) => 'fb:' + p
|
|
})
|
|
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('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' },
|
|
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'))
|
|
})
|
|
|
|
test('pseudo file bytes version and shm', (t) => {
|
|
const shm = new Map()
|
|
shm.set('q', new Uint8Array([9]))
|
|
const fn = createVfsPseudoFileBytes({
|
|
bareOsDevShm: shm,
|
|
pseudoVersionText: () => 'Bare OS\n',
|
|
pseudoUrandomBytes: () => new Uint8Array([1])
|
|
})
|
|
const ver = fn({ file: 'version', kind: 'proc' })
|
|
t.ok(ver.byteLength > 0)
|
|
const empty = fn({ file: 'null', kind: 'dev' })
|
|
t.is(empty.byteLength, 0)
|
|
const got = fn({ file: 'shm', kind: 'dev', shmName: 'q' })
|
|
t.is(got[0], 9)
|
|
})
|
|
|
|
test('lifecycle hooks register invoke and reload', async (t) => {
|
|
const suspendHooks = []
|
|
const resumeHooks = []
|
|
const m = createBareOsLifecycleHookMethods({
|
|
suspendHooks,
|
|
resumeHooks,
|
|
env: { BARE_OS_KERNEL_PROFILE_WARM: '1' }
|
|
})
|
|
let n = 0
|
|
m.bareOsRegisterSuspendHook(() => {
|
|
n++
|
|
})
|
|
await m.bareOsInvokeSuspendHooks()
|
|
t.is(n, 1)
|
|
t.exception(() => m.bareOsRequestKernelReload(), /BARE_OS_KERNEL_RELOAD/)
|
|
t.exception(
|
|
() => m.bareOsRequestKernelProfileReload(),
|
|
/BARE_OS_KERNEL_PROFILE_RELOAD/
|
|
)
|
|
})
|
|
|
|
test('process control existence probe and renice', (t) => {
|
|
const delivered = []
|
|
const nice = new Map()
|
|
const ctx = {
|
|
bareOsLogicalSigaction: {},
|
|
...createBareOsProcessControlMethods({
|
|
deliverBareOsVirtualSignalToPid: (_c, pid, sig) => {
|
|
delivered.push([pid, sig])
|
|
return { delivered: true, atMs: 1 }
|
|
},
|
|
getInteractiveCtx: () => ({ shellBackgroundJobs: { list: [] } }),
|
|
niceByPid: nice
|
|
})
|
|
}
|
|
const z = ctx.bareOsSendSignal('kernel', '0')
|
|
t.ok(z.exists)
|
|
t.is(z.pid, 1)
|
|
const r = ctx.bareOsRenice(1, 2)
|
|
t.is(r.nice, 2)
|
|
t.is(nice.get(1), 2)
|
|
})
|