Updates
This commit is contained in:
@@ -223,11 +223,17 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
GROUP: 'guest',
|
||||
BARE_OS_IDENTITY: 'guest',
|
||||
BARE_OS_EXIT_STATUS: '0',
|
||||
BARE_OS_CTX_API_VERSION,
|
||||
0: 'bare-os'
|
||||
}
|
||||
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
|
||||
const vfsMountRef = { getMounts: () => new Map() }
|
||||
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef)
|
||||
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
|
||||
procSnapshot: {
|
||||
version: BARE_OS_CTX_API_VERSION,
|
||||
cmdline: 'bare-os-booter'
|
||||
}
|
||||
})
|
||||
|
||||
const hdmsController = new HdmsController()
|
||||
disk.hdmsController = hdmsController
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* systemctl-compatible CLI for bare-initd units. Invoked from kernel-runner delegation.
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
import { INITD_LOG } from './bare-os-var-log.js'
|
||||
import {
|
||||
findBareServiceDefinition,
|
||||
@@ -15,8 +16,7 @@ import {
|
||||
/** @param {Uint8Array | null} buf @param {number} maxLines */
|
||||
function tailUtf8Lines(buf, maxLines) {
|
||||
if (!buf || !buf.length) return ''
|
||||
const dec = new TextDecoder('utf-8', { fatal: false })
|
||||
const text = dec.decode(buf)
|
||||
const text = b4a.toString(buf, 'utf8')
|
||||
const lines = text.split(/\r?\n/)
|
||||
if (lines.length <= maxLines) return text.trimEnd()
|
||||
return lines.slice(-maxLines).join('\n')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import b4a from 'b4a'
|
||||
/**
|
||||
* POSIX-like stat metadata stored in Hyperdrive entry value.metadata.bareOs (JSON).
|
||||
*/
|
||||
@@ -178,7 +179,14 @@ export function newBareOsForSymlink(env) {
|
||||
* @param {{ drive: import('hyperdrive').default, virtualHomeDir?: boolean, virtualMntRoot?: boolean, virtualVarRoot?: boolean }} r
|
||||
*/
|
||||
export function isPersonalRoute(personalDrive, r) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return false
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return r.drive === personalDrive
|
||||
}
|
||||
|
||||
@@ -200,8 +208,7 @@ export function synthesizeStat(
|
||||
const nlink = 1
|
||||
if (type === 'symlink') {
|
||||
const linkname = entryHints.linkname || ''
|
||||
const enc = new TextEncoder()
|
||||
const size = enc.encode(linkname).length
|
||||
const size = b4a.from(linkname, 'utf8').length
|
||||
const { uid, gid } = personal ? parseUidGid(env) : { uid: 0, gid: 0 }
|
||||
const { user, group } = personal
|
||||
? identityNames(env)
|
||||
@@ -310,6 +317,7 @@ export function statFromBareOs(bo, type, size, abs, linkname) {
|
||||
* @param {string} abs
|
||||
*/
|
||||
export function isVirtualMountPoint(abs) {
|
||||
const n = abs.replace(/\/+$/, '') || '/'
|
||||
return (
|
||||
abs === '/' ||
|
||||
abs === '/home' ||
|
||||
@@ -317,7 +325,12 @@ export function isVirtualMountPoint(abs) {
|
||||
abs === '/var' ||
|
||||
abs === '/home/' ||
|
||||
abs === '/mnt/' ||
|
||||
abs === '/var/'
|
||||
abs === '/var/' ||
|
||||
n === '/proc' ||
|
||||
n === '/proc/self' ||
|
||||
n === '/sys' ||
|
||||
n === '/sys/fs' ||
|
||||
n === '/sys/fs/bare_os'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import unixPathResolve from 'unix-path-resolve'
|
||||
import b4a from 'b4a'
|
||||
import {
|
||||
extractBareOs,
|
||||
isPersonalRoute,
|
||||
@@ -22,12 +23,21 @@ const DIR_MARKER = '.bareos_empty'
|
||||
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
|
||||
* optional HDMS mounts under /mnt/<label>/…, virtual /var with writable /var/log/…
|
||||
* on the personal drive under /.bare-os/var/log/<home-seg>/… (session-isolated).
|
||||
* Read-only pseudo `proc` and `sys` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive.
|
||||
* @param {import('hyperdrive').default} systemDrive
|
||||
* @param {import('hyperdrive').default} personalDrive
|
||||
* @param {Record<string, string>} env
|
||||
* @param {{ getMounts?: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> } | null} [mntRef]
|
||||
* @param {{ procSnapshot?: { version?: string, cmdline?: string } }} [vfsOptions]
|
||||
*/
|
||||
export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
export function createVfs(
|
||||
systemDrive,
|
||||
personalDrive,
|
||||
env,
|
||||
mntRef = null,
|
||||
vfsOptions = {}
|
||||
) {
|
||||
const procSnapshot = vfsOptions.procSnapshot || null
|
||||
const HOME = () => env.HOME || '/home/guest'
|
||||
let cwd = env.PWD || HOME()
|
||||
|
||||
@@ -56,6 +66,135 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
return seg ? `/.bare-os/var/log/${seg}` : '/.bare-os/var/log/_nosession'
|
||||
}
|
||||
|
||||
/** Session-isolated writable `/tmp` on the personal drive. */
|
||||
function tmpStorageRoot() {
|
||||
const seg = activeHomeBasename()
|
||||
return seg ? `/.bare-os/tmp/${seg}` : '/.bare-os/tmp/_nosession'
|
||||
}
|
||||
|
||||
const ENV_SECRET_HINT = new RegExp(
|
||||
'PASSWORD|SECRET|TOKEN|AUTH|KEY|VAULT|PRIVATE|CREDENTIAL|PASSPHRASE',
|
||||
'i'
|
||||
)
|
||||
|
||||
const ENV_PUBLIC_KEYS = new Set([
|
||||
'HOME',
|
||||
'USER',
|
||||
'LOGNAME',
|
||||
'PATH',
|
||||
'PWD',
|
||||
'SHELL',
|
||||
'HOSTNAME',
|
||||
'UID',
|
||||
'GID',
|
||||
'GROUP',
|
||||
'TERM',
|
||||
'LANG',
|
||||
'LC_ALL',
|
||||
'EDITOR',
|
||||
'BARE_OS_EXIT_STATUS',
|
||||
'BARE_OS_IDENTITY',
|
||||
'BARE_OS_CTX_API_VERSION'
|
||||
])
|
||||
|
||||
function environKeyAllowed(k) {
|
||||
if (ENV_SECRET_HINT.test(k)) return false
|
||||
if (ENV_PUBLIC_KEYS.has(k)) return true
|
||||
if (k.startsWith('BARE_OS_')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function pseudoVersionText() {
|
||||
const v =
|
||||
procSnapshot?.version ??
|
||||
env.BARE_OS_CTX_API_VERSION ??
|
||||
'unknown'
|
||||
return `Bare OS\nbare_os_ctx_api_version=${v}\n`
|
||||
}
|
||||
|
||||
function pseudoCmdlineText() {
|
||||
return `${procSnapshot?.cmdline ?? 'bare-os'}\n`
|
||||
}
|
||||
|
||||
/** UTF-8 bytes; Bare may not define global TextEncoder (see curl-cli utf8Encode). */
|
||||
function utf8Encode(str) {
|
||||
return b4a.from(String(str), 'utf8')
|
||||
}
|
||||
|
||||
function pseudoEnvironBytes() {
|
||||
const parts = []
|
||||
for (const k of Object.keys(env).sort()) {
|
||||
if (!environKeyAllowed(k)) continue
|
||||
const v = env[k]
|
||||
if (typeof v !== 'string') continue
|
||||
parts.push(`${k}=${v}\0`)
|
||||
}
|
||||
return utf8Encode(parts.join(''))
|
||||
}
|
||||
|
||||
function pseudoFileBytes(routePseudo) {
|
||||
const f = routePseudo.file
|
||||
if (f === 'version') return utf8Encode(pseudoVersionText())
|
||||
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
|
||||
if (f === 'environ') return pseudoEnvironBytes()
|
||||
return utf8Encode('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} absPath
|
||||
* @returns {null | Record<string, unknown>}
|
||||
*/
|
||||
function classifyPseudoAbs(absPath) {
|
||||
const n = absPath.replace(/\/+$/, '') || '/'
|
||||
if (n === '/proc' || n.startsWith('/proc/')) {
|
||||
if (n === '/proc') {
|
||||
return { virtualPseudo: true, kind: 'proc', node: 'root' }
|
||||
}
|
||||
const sub = n.slice(6)
|
||||
if (sub === 'version' || sub === 'bare_os_version') {
|
||||
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'version' }
|
||||
}
|
||||
if (sub === 'self') {
|
||||
return { virtualPseudo: true, kind: 'proc', node: 'dir', dir: 'self' }
|
||||
}
|
||||
if (sub === 'self/environ') {
|
||||
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'environ' }
|
||||
}
|
||||
if (sub === 'self/cmdline') {
|
||||
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'cmdline' }
|
||||
}
|
||||
return { virtualPseudo: true, kind: 'proc', node: 'enoent' }
|
||||
}
|
||||
if (n === '/sys' || n.startsWith('/sys/')) {
|
||||
if (n === '/sys') {
|
||||
return { virtualPseudo: true, kind: 'sys', node: 'root' }
|
||||
}
|
||||
if (n === '/sys/fs') {
|
||||
return { virtualPseudo: true, kind: 'sys', node: 'dir', dir: 'fs' }
|
||||
}
|
||||
if (n === '/sys/fs/bare_os') {
|
||||
return { virtualPseudo: true, kind: 'sys', node: 'dir', dir: 'bare_os' }
|
||||
}
|
||||
if (n === '/sys/fs/bare_os/version') {
|
||||
return { virtualPseudo: true, kind: 'sys', node: 'file', file: 'version' }
|
||||
}
|
||||
return { virtualPseudo: true, kind: 'sys', node: 'enoent' }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function lstatVirtualPseudo(abs, r) {
|
||||
if (!r.virtualPseudo) return null
|
||||
if (r.node === 'enoent') return null
|
||||
if (r.node === 'root' || r.node === 'dir') {
|
||||
return { ...synthesizeStat(abs, false, env, 'directory'), path: abs }
|
||||
}
|
||||
const body = pseudoFileBytes(r)
|
||||
const st = { ...synthesizeStat(abs, false, env, 'file'), path: abs }
|
||||
st.size = body.byteLength
|
||||
return st
|
||||
}
|
||||
|
||||
/**
|
||||
* `cd ~`, `touch ~/x`, etc. must map to $HOME — otherwise `unix-path-resolve(cwd, '~')`
|
||||
* becomes `/~` on the system drive (read-only).
|
||||
@@ -112,6 +251,20 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
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 }
|
||||
@@ -179,7 +332,15 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async function isRegularFile(absPath) {
|
||||
if (absPath === '/') return false
|
||||
const r = route(absPath)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return false
|
||||
if (r.virtualPseudo && r.node === 'file') return true
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return false
|
||||
const e = await entryOn(drive, p, { follow: true })
|
||||
@@ -225,8 +386,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
if (!v) return null
|
||||
if (v.linkname) {
|
||||
const linkname = v.linkname
|
||||
const enc = new TextEncoder()
|
||||
const size = enc.encode(String(linkname)).length
|
||||
const size = utf8Encode(String(linkname)).length
|
||||
const bo = extractBareOs(v)
|
||||
if (bo) return statFromBareOs(bo, 'symlink', size, abs, linkname)
|
||||
return synthesizeStat(abs, personal, env, 'symlink', { linkname })
|
||||
@@ -265,6 +425,9 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
if (r.virtualVarRoot) {
|
||||
return { ...synthesizeStat(abs, true, env, 'directory'), path: abs }
|
||||
}
|
||||
if (r.virtualPseudo) {
|
||||
return lstatVirtualPseudo(abs, r)
|
||||
}
|
||||
const { drive, path: p } = r
|
||||
const personal = isPersonalRoute(personalDrive, r)
|
||||
if (isHyperdriveRootPath(p)) {
|
||||
@@ -295,6 +458,14 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
) {
|
||||
return { ...synthesizeStat(abs, true, env, 'directory'), path: abs }
|
||||
}
|
||||
const normTmp = abs.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
normTmp === '/tmp' &&
|
||||
r.drive === personalDrive &&
|
||||
p === tmpStorageRoot()
|
||||
) {
|
||||
return { ...synthesizeStat(abs, true, env, 'directory'), path: abs }
|
||||
}
|
||||
const h = normalizeHome()
|
||||
const homeNorm = h.replace(/\/+$/, '') || h
|
||||
const absNorm = abs.replace(/\/+$/, '') || abs
|
||||
@@ -306,6 +477,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
|
||||
async function statFromAbs(abs) {
|
||||
const normVar = abs.replace(/\/+$/, '') || '/'
|
||||
const normTmp = abs.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
abs === '/mnt' ||
|
||||
abs === '/mnt/' ||
|
||||
@@ -313,12 +485,15 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
abs === '/' ||
|
||||
normVar === '/var' ||
|
||||
normVar === '/var/log' ||
|
||||
normTmp === '/tmp' ||
|
||||
(activeHomeBasename() && abs === '/home')
|
||||
) {
|
||||
return lstatFromAbs(abs)
|
||||
}
|
||||
const r0 = route(abs)
|
||||
if (r0.virtualMntRoot || r0.virtualVarRoot) return lstatFromAbs(abs)
|
||||
if (r0.virtualMntRoot || r0.virtualVarRoot || r0.virtualPseudo) {
|
||||
return lstatFromAbs(abs)
|
||||
}
|
||||
let cur = abs
|
||||
for (let depth = 0; depth < 16; depth++) {
|
||||
const curNorm = cur.replace(/\/+$/, '') || '/'
|
||||
@@ -327,12 +502,15 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
cur === '/home' ||
|
||||
cur === '/mnt' ||
|
||||
curNorm === '/var' ||
|
||||
curNorm === '/var/log'
|
||||
curNorm === '/var/log' ||
|
||||
curNorm === '/tmp'
|
||||
) {
|
||||
return lstatFromAbs(cur)
|
||||
}
|
||||
const r = route(cur)
|
||||
if (r.virtualMntRoot || r.virtualVarRoot) return lstatFromAbs(cur)
|
||||
if (r.virtualMntRoot || r.virtualVarRoot || r.virtualPseudo) {
|
||||
return lstatFromAbs(cur)
|
||||
}
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return lstatFromAbs(cur)
|
||||
const e = await entryOn(drive, p, { follow: false })
|
||||
@@ -466,6 +644,30 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
names.add('log')
|
||||
return [...names].sort()
|
||||
}
|
||||
const pr = route(abs)
|
||||
if (pr.virtualPseudo) {
|
||||
if (pr.node === 'enoent') {
|
||||
throw new Error('ENOENT: ' + abs)
|
||||
}
|
||||
if (pr.node === 'file') {
|
||||
throw new Error('Not a directory: ' + abs)
|
||||
}
|
||||
if (pr.kind === 'proc' && pr.node === 'root') {
|
||||
return ['bare_os_version', 'self', 'version']
|
||||
}
|
||||
if (pr.kind === 'proc' && pr.node === 'dir' && pr.dir === 'self') {
|
||||
return ['cmdline', 'environ']
|
||||
}
|
||||
if (pr.kind === 'sys' && pr.node === 'root') {
|
||||
return ['fs']
|
||||
}
|
||||
if (pr.kind === 'sys' && pr.node === 'dir' && pr.dir === 'fs') {
|
||||
return ['bare_os']
|
||||
}
|
||||
if (pr.kind === 'sys' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
||||
return ['version']
|
||||
}
|
||||
}
|
||||
const r = route(abs)
|
||||
if (r.virtualMntRoot) {
|
||||
return [...getMntMap().keys()].sort()
|
||||
@@ -486,12 +688,26 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
if (abs === '/' && !names.includes('var')) {
|
||||
names.push('var')
|
||||
}
|
||||
if (abs === '/' && !names.includes('proc')) {
|
||||
names.push('proc')
|
||||
}
|
||||
if (abs === '/' && !names.includes('sys')) {
|
||||
names.push('sys')
|
||||
}
|
||||
if (abs === '/' && !names.includes('tmp')) {
|
||||
names.push('tmp')
|
||||
}
|
||||
return names.sort()
|
||||
}
|
||||
|
||||
async function delFromAbs(abs) {
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -518,7 +734,12 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
*/
|
||||
async function rmFromAbs(abs, { recursive = false, force = false } = {}) {
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
if (force) return
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
@@ -552,7 +773,12 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
*/
|
||||
async function writeFileAtAbs(abs, buf, opts = {}) {
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -614,6 +840,17 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async readFile(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualPseudo) {
|
||||
if (r.node === 'enoent') {
|
||||
await assertTraverseTo(abs, 'read')
|
||||
return null
|
||||
}
|
||||
if (r.node === 'file') {
|
||||
await assertTraverseTo(abs, 'read')
|
||||
return pseudoFileBytes(r)
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return null
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return null
|
||||
@@ -628,7 +865,12 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async unlink(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + userPath)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -668,6 +910,10 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async exists(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualPseudo) {
|
||||
if (r.node === 'enoent') return false
|
||||
return true
|
||||
}
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return true
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return true
|
||||
@@ -694,7 +940,12 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async chmod(userPath, modeOctal) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
throw new Error('chmod: ' + userPath + ': Operation not supported')
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -825,7 +1076,12 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async readlink(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
throw new Error('EINVAL readlink')
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -845,7 +1101,12 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async symlink(target, userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
r.virtualMntRoot ||
|
||||
r.virtualVarRoot ||
|
||||
r.virtualPseudo
|
||||
) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + userPath)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
|
||||
@@ -435,6 +435,117 @@ test('vfs lists virtual /home at root; /home shows only active session dir', asy
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
||||
const dir = testCorestoreDir('vfsproc')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pvproc'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const env = {
|
||||
HOME: '/home/guest',
|
||||
PWD: '/home/guest',
|
||||
PATH: '/bin',
|
||||
USER: 'guest',
|
||||
BARE_OS_CTX_API_VERSION: '9.9.9-test'
|
||||
}
|
||||
const vfs = createVfs(sys, personal, env, null, {
|
||||
procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' }
|
||||
})
|
||||
const root = await vfs.readdir('/')
|
||||
t.ok(root.includes('proc'))
|
||||
t.ok(root.includes('sys'))
|
||||
t.ok(root.includes('tmp'))
|
||||
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
|
||||
'bare_os_version',
|
||||
'self',
|
||||
'version'
|
||||
])
|
||||
t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [
|
||||
'cmdline',
|
||||
'environ'
|
||||
])
|
||||
const ver = b4a.toString(await vfs.readFile('/proc/version'))
|
||||
t.ok(ver.includes('Bare OS'))
|
||||
t.ok(ver.includes('bare_os_ctx_api_version=1.2.3-test'))
|
||||
const sysVer = b4a.toString(
|
||||
await vfs.readFile('/sys/fs/bare_os/version')
|
||||
)
|
||||
t.ok(sysVer.includes('1.2.3-test'))
|
||||
const cmd = b4a.toString(await vfs.readFile('/proc/self/cmdline'))
|
||||
t.ok(cmd.includes('unit-test'))
|
||||
let writeErr = null
|
||||
try {
|
||||
await vfs.writeFile('/proc/version', b4a.from('x'))
|
||||
} catch (e) {
|
||||
writeErr = e
|
||||
}
|
||||
t.ok(writeErr)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs /proc/self/environ omits secret-like env keys', async (t) => {
|
||||
const dir = testCorestoreDir('vfsprocenv')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pvpe'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const env = {
|
||||
HOME: '/home/guest',
|
||||
PWD: '/home/guest',
|
||||
PATH: '/bin',
|
||||
USER: 'guest',
|
||||
MY_PASSWORD: 'hunter2',
|
||||
BARE_OS_SAFE: 'visible'
|
||||
}
|
||||
const vfs = createVfs(sys, personal, env)
|
||||
const raw = b4a.toString(await vfs.readFile('/proc/self/environ'))
|
||||
t.ok(raw.includes('HOME='))
|
||||
t.ok(raw.includes('BARE_OS_SAFE='))
|
||||
t.ok(!raw.includes('MY_PASSWORD'))
|
||||
t.ok(!raw.includes('hunter2'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs /tmp maps to personal drive per HOME basename', async (t) => {
|
||||
const dir = testCorestoreDir('vfstmp')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pvtmp'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const vfsGuest = createVfs(sys, personal, {
|
||||
HOME: '/home/guest',
|
||||
PWD: '/home/guest',
|
||||
PATH: '/bin'
|
||||
})
|
||||
await vfsGuest.writeFile('/tmp/session.dat', b4a.from('g'))
|
||||
t.is(
|
||||
b4a.toString(await personal.get('/.bare-os/tmp/guest/session.dat')),
|
||||
'g'
|
||||
)
|
||||
const vfsUser = createVfs(sys, personal, {
|
||||
HOME: '/home/hexuserdead',
|
||||
PWD: '/home/hexuserdead',
|
||||
PATH: '/bin'
|
||||
})
|
||||
t.is(await vfsUser.readFile('/tmp/session.dat'), null)
|
||||
await vfsUser.writeFile('/tmp/session.dat', b4a.from('u'))
|
||||
t.is(
|
||||
b4a.toString(await personal.get('/.bare-os/tmp/hexuserdead/session.dat')),
|
||||
'u'
|
||||
)
|
||||
t.is(
|
||||
b4a.toString(await personal.get('/.bare-os/tmp/guest/session.dat')),
|
||||
'g'
|
||||
)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs isolates home and /var/log per HOME basename on personal drive', async (t) => {
|
||||
const dir = testCorestoreDir('vfsisol')
|
||||
const store = new Corestore(dir)
|
||||
|
||||
@@ -1198,7 +1198,7 @@ async function bareAwkRun(program, opts, io) {
|
||||
} else {
|
||||
for (const f of files) {
|
||||
const buf = await io.readFile(f)
|
||||
const text = buf ? new TextDecoder().decode(buf) : ''
|
||||
const text = buf && io.bytesToString ? io.bytesToString(buf) : ''
|
||||
const lines = text.split(/\r?\n/)
|
||||
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
||||
await runOnFile(f, lines)
|
||||
|
||||
@@ -64,6 +64,9 @@ async function run(ctx, argv) {
|
||||
})()
|
||||
|
||||
const io = {
|
||||
bytesToString(buf) {
|
||||
return buf ? ctx.b4a.toString(buf, 'utf8') : ''
|
||||
},
|
||||
print(s) {
|
||||
const t = s.replace(/\n$/, '')
|
||||
ctx.console.log(t)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
function count(s) {
|
||||
function count(s, b4a) {
|
||||
const lines = (s.match(/\n/g) || []).length
|
||||
const words = s.trim() ? s.trim().split(/\s+/).length : 0
|
||||
const bytes = new TextEncoder().encode(s).length
|
||||
const bytes = b4a.from(s, 'utf8').length
|
||||
return { lines, words, bytes }
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ async function run(ctx, argv) {
|
||||
const files = argv.slice(1).filter((a) => !a.startsWith('-'))
|
||||
if (!files.length) {
|
||||
const s = bareStdin(ctx)
|
||||
const c = count(s)
|
||||
const c = count(s, ctx.b4a)
|
||||
ctx.console.log(' ' + c.lines + ' ' + c.words + ' ' + c.bytes)
|
||||
return
|
||||
}
|
||||
@@ -24,7 +24,7 @@ async function run(ctx, argv) {
|
||||
continue
|
||||
}
|
||||
const s = ctx.b4a.toString(buf)
|
||||
const c = count(s)
|
||||
const c = count(s, ctx.b4a)
|
||||
tLines += c.lines
|
||||
tWords += c.words
|
||||
tBytes += c.bytes
|
||||
|
||||
@@ -1260,7 +1260,7 @@ async function bareAwkRun(program, opts, io) {
|
||||
} else {
|
||||
for (const f of files) {
|
||||
const buf = await io.readFile(f)
|
||||
const text = buf ? new TextDecoder().decode(buf) : ''
|
||||
const text = buf && io.bytesToString ? io.bytesToString(buf) : ''
|
||||
const lines = text.split(/\r?\n/)
|
||||
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
||||
await runOnFile(f, lines)
|
||||
@@ -1346,6 +1346,9 @@ async function run(ctx, argv) {
|
||||
})()
|
||||
|
||||
const io = {
|
||||
bytesToString(buf) {
|
||||
return buf ? ctx.b4a.toString(buf, 'utf8') : ''
|
||||
},
|
||||
print(s) {
|
||||
const t = s.replace(/\n$/, '')
|
||||
ctx.console.log(t)
|
||||
|
||||
@@ -60,10 +60,10 @@ function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
function count(s) {
|
||||
function count(s, b4a) {
|
||||
const lines = (s.match(/\n/g) || []).length
|
||||
const words = s.trim() ? s.trim().split(/\s+/).length : 0
|
||||
const bytes = new TextEncoder().encode(s).length
|
||||
const bytes = b4a.from(s, 'utf8').length
|
||||
return { lines, words, bytes }
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ async function run(ctx, argv) {
|
||||
const files = argv.slice(1).filter((a) => !a.startsWith('-'))
|
||||
if (!files.length) {
|
||||
const s = bareStdin(ctx)
|
||||
const c = count(s)
|
||||
const c = count(s, ctx.b4a)
|
||||
ctx.console.log(' ' + c.lines + ' ' + c.words + ' ' + c.bytes)
|
||||
return
|
||||
}
|
||||
@@ -86,7 +86,7 @@ async function run(ctx, argv) {
|
||||
continue
|
||||
}
|
||||
const s = ctx.b4a.toString(buf)
|
||||
const c = count(s)
|
||||
const c = count(s, ctx.b4a)
|
||||
tLines += c.lines
|
||||
tWords += c.words
|
||||
tBytes += c.bytes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Optional boot-time shell lines (one command per non-comment line).
|
||||
# Executed by /boot/init.js after /etc/os-release and /etc/motd, then /etc/bare-os/rc.d/*, before the main banner.
|
||||
# Executed by /boot/init.js after /etc/os-release and /etc/motd, then /etc/bare-os/rc.d/* (digit-prefixed names only), before the main banner.
|
||||
# Example (uncomment to use):
|
||||
# export BARE_OS_SHOW_RC=1
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Optional boot snippets (trusted)
|
||||
|
||||
Only **filenames that start with a digit** are executed (e.g. `10-local`,
|
||||
`20-proxy`), after `/etc/bare-os/rc`, in **lexicographic order**. This keeps
|
||||
documentation files (this file is named `.README` so even older kernels skip it)
|
||||
from being run as shell.
|
||||
|
||||
Each snippet is treated like `rc`: one shell command per non-empty, non-comment
|
||||
line. Lines starting with `#` and blank lines are ignored.
|
||||
|
||||
Dotfiles, names ending in `~`, `README*`, `*.md`, and any name not starting with
|
||||
a digit are skipped.
|
||||
|
||||
Only ship snippets you trust — they run with full `execLine` power.
|
||||
@@ -1,11 +0,0 @@
|
||||
# Optional boot snippets (trusted)
|
||||
|
||||
Files in this directory are executed after `/etc/bare-os/rc`, in **lexicographic
|
||||
order** by filename. Use numeric prefixes (e.g. `10-local`, `20-proxy`) to
|
||||
control order. Each file is treated like `rc`: one shell command per non-empty,
|
||||
non-comment line.
|
||||
|
||||
Lines starting with `#` and blank lines are ignored. Dotfiles and names ending
|
||||
in `~` are skipped.
|
||||
|
||||
Only ship snippets you trust — they run with full `execLine` power.
|
||||
@@ -66,9 +66,20 @@ async function runRcFileAt(ctx, drivePath, label) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only run rc.d files whose name starts with a digit (e.g. `10-local`).
|
||||
* Skips README*, *.md, dotfiles, and *~ so documentation is never exec'd as shell.
|
||||
* @param {string} name basename from readdir
|
||||
*/
|
||||
function isBareOsRcSnippetFile(name) {
|
||||
if (!name || name.startsWith('.') || name.endsWith('~')) return false
|
||||
if (/^README(\.|$)/i.test(name)) return false
|
||||
if (/\.md$/i.test(name)) return false
|
||||
return /^[0-9]/.test(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional snippets under /etc/bare-os/rc.d/ — executed in lexicographic order.
|
||||
* Skips dotfiles and names ending in ~.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
async function runBareOsRcDir(ctx) {
|
||||
@@ -83,7 +94,7 @@ async function runBareOsRcDir(ctx) {
|
||||
}
|
||||
names.sort()
|
||||
for (const name of names) {
|
||||
if (!name || name.startsWith('.') || name.endsWith('~')) continue
|
||||
if (!isBareOsRcSnippetFile(name)) continue
|
||||
const p = `/etc/bare-os/rc.d/${name}`
|
||||
try {
|
||||
const buf = await drive.get(p)
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user