This commit is contained in:
Raven Scott
2026-04-03 19:01:57 -04:00
parent 7c9c9ae83c
commit 8002a66f6e
28 changed files with 533 additions and 78 deletions
+7 -1
View File
@@ -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 -2
View File
@@ -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')
+17 -4
View File
@@ -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'
)
}
+275 -14
View File
@@ -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) {
+111
View File
@@ -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)
+1 -1
View File
@@ -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)
+3
View File
@@ -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)
+4 -4
View File
@@ -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
+4 -1
View File
@@ -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)
+4 -4
View File
@@ -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.
+13 -2
View File
@@ -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