Files
bare-operating-system/packages/bare-os-booter/lib/vfs-posix-meta.js
T
Raven Scott 92384990c5 feat(booter): POSIX/P2P roadmap — syscalls v4, mirror VFS, seeder snapshot hints
- Bump ctx API to 1.31.0; extend /proc/bare_os/syscalls.json schema 4 (fdModel, signalModel)
- Add /mirror/aux* routing for auxiliary Hyperdrives; optional BLAKE2b-keyed /bin cache (BARE_OS_VFS_BIN_CACHE_BLAKE2B)
- Wire seeder snapshotHintsJson from BARE_OS_SEED_* env; add seeder unit tests
- Shell: trap -p; docs for jobs/bg/trap; sync schemas, examples, compatibility matrix, handbook ch.9
- Coreutils: Issue 7 man option rows for cp/mv/ln/find/grep/sed/awk/tar/test/true; xcu-issue7-sweep tests
- Docs: env appendix, vault-threat-model, node-vs-bare-host-matrix, test:bare, CHANGELOG 1.31.0
2026-04-04 22:00:01 -04:00

439 lines
10 KiB
JavaScript

import b4a from 'b4a'
/**
* POSIX-like stat metadata stored in Hyperdrive entry value.metadata.bareOs (JSON).
*/
/** @typedef {{ mode: number, uid: number, gid: number, uname: string, gname: string, mtimeMs: number, ctimeMs: number }} BareOsMeta */
export const S_IFDIR = 0o040000
export const S_IFREG = 0o100000
export const S_IFLNK = 0o120000
const DEFAULT_UMASK = 0o022
/**
* @param {Record<string, string>} env
*/
export function parseUmask(env) {
const raw = env.UMASK
if (raw == null || raw === '') return DEFAULT_UMASK
const s = String(raw).trim()
const m = /^0?([0-7]{1,3})$/.exec(s)
if (m) return Number.parseInt(m[1], 8) & 0o777
return DEFAULT_UMASK
}
/**
* @param {Record<string, string>} env
*/
export function parseUidGid(env) {
const uid = Number.parseInt(String(env.UID ?? '0'), 10)
const gid = Number.parseInt(String(env.GID ?? '0'), 10)
return {
uid: Number.isFinite(uid) ? uid : 0,
gid: Number.isFinite(gid) ? gid : 0
}
}
/**
* @param {Record<string, string>} env
*/
export function identityNames(env) {
const user = env.USER || env.LOGNAME || 'root'
const group = env.GROUP || user
return { user, group }
}
/**
* @param {unknown} value Hyperdrive entry .value
* @returns {BareOsMeta | null}
*/
export function extractBareOs(value) {
if (!value || typeof value !== 'object') return null
const md = /** @type {Record<string, unknown>} */ (value).metadata
if (!md || typeof md !== 'object') return null
const b = /** @type {Record<string, unknown>} */ (md).bareOs
if (!b || typeof b !== 'object') return null
const mode = Number(b.mode)
const uid = Number(b.uid)
const gid = Number(b.gid)
if (!Number.isFinite(mode)) return null
return {
mode: mode & 0xffff,
uid: Number.isFinite(uid) ? uid : 0,
gid: Number.isFinite(gid) ? gid : 0,
uname: typeof b.uname === 'string' ? b.uname : 'root',
gname: typeof b.gname === 'string' ? b.gname : 'root',
mtimeMs:
typeof b.mtimeMs === 'number' && Number.isFinite(b.mtimeMs)
? b.mtimeMs
: Date.now(),
ctimeMs:
typeof b.ctimeMs === 'number' && Number.isFinite(b.ctimeMs)
? b.ctimeMs
: Date.now()
}
}
/**
* @param {Record<string, unknown> | null | undefined} existingMetadata
* @param {BareOsMeta} bareOs
*/
export function mergeEntryMetadata(existingMetadata, bareOs) {
const base =
existingMetadata && typeof existingMetadata === 'object'
? { ...existingMetadata }
: {}
return { ...base, bareOs }
}
/**
* @param {Record<string, string>} env
* @param {{ executable?: boolean }} [opts]
* @returns {BareOsMeta}
*/
export function newBareOsForFile(env, opts = {}) {
const umask = parseUmask(env)
const { uid, gid } = parseUidGid(env)
const { user, group } = identityNames(env)
const now = Date.now()
let mode = S_IFREG | (0o666 & ~umask)
if (opts.executable) mode |= 0o111
return {
mode,
uid,
gid,
uname: user,
gname: group,
mtimeMs: now,
ctimeMs: now
}
}
/**
* @param {BareOsMeta | null} prev
* @param {Record<string, string>} env
* @param {{
* executable?: boolean,
* bumpMtime?: boolean,
* touchCtime?: boolean,
* legacyExecutable?: boolean,
* mtimeMs?: number,
* ctimeMs?: number,
* posixModeBits?: number
* }} [opts]
* @returns {BareOsMeta}
*/
export function mergeBareOsOnWrite(prev, env, opts = {}) {
const now = Date.now()
const pickM = (def) =>
typeof opts.mtimeMs === 'number' && Number.isFinite(opts.mtimeMs)
? opts.mtimeMs
: def
const pickC = (def) =>
typeof opts.ctimeMs === 'number' && Number.isFinite(opts.ctimeMs)
? opts.ctimeMs
: def
if (!prev) {
const umask = parseUmask(env)
const { uid, gid } = parseUidGid(env)
const { user, group } = identityNames(env)
const legacyEx = !!opts.legacyExecutable
const wantEx = opts.executable !== undefined ? opts.executable : legacyEx
let mode =
typeof opts.posixModeBits === 'number' && Number.isFinite(opts.posixModeBits)
? S_IFREG | (opts.posixModeBits & 0o777)
: S_IFREG | (0o666 & ~umask)
if (wantEx) mode |= 0o111
return {
mode,
uid,
gid,
uname: user,
gname: group,
mtimeMs: pickM(now),
ctimeMs: pickC(now)
}
}
const executable =
opts.executable !== undefined ? opts.executable : !!(prev.mode & 0o111)
let mode = prev.mode
if (opts.executable !== undefined) {
if (executable) mode |= 0o111
else mode &= ~0o111
}
const mtimeMs = pickM(opts.bumpMtime !== false ? now : prev.mtimeMs)
const ctimeMs = pickC(opts.touchCtime ? now : prev.ctimeMs)
return {
mode,
uid: prev.uid,
gid: prev.gid,
uname: prev.uname,
gname: prev.gname,
mtimeMs,
ctimeMs
}
}
/**
* @param {Record<string, string>} env
* @returns {BareOsMeta}
*/
export function newBareOsForSymlink(env) {
const { uid, gid } = parseUidGid(env)
const { user, group } = identityNames(env)
const now = Date.now()
return {
mode: S_IFLNK | 0o777,
uid,
gid,
uname: user,
gname: group,
mtimeMs: now,
ctimeMs: now
}
}
/**
* @param {import('hyperdrive').default} personalDrive
* @param {{ drive: import('hyperdrive').default, virtualHomeDir?: boolean, virtualMntRoot?: boolean, virtualVarRoot?: boolean }} r
*/
export function isPersonalRoute(personalDrive, r) {
if (
r.virtualHomeDir ||
r.virtualMntRoot ||
r.virtualVarRoot ||
r.virtualPseudo
) {
return false
}
return r.drive === personalDrive
}
/**
* @param {string} abs absolute logical path
* @param {boolean} personal
* @param {Record<string, string>} env
* @param {'file' | 'directory' | 'symlink'} type
* @param {{ executable?: boolean, linkname?: string }} [entryHints]
*/
export function synthesizeStat(abs, personal, env, type, entryHints = {}) {
const now = Date.now()
const nlink = 1
if (type === 'symlink') {
const linkname = entryHints.linkname || ''
const size = b4a.from(linkname, 'utf8').length
const { uid, gid } = personal ? parseUidGid(env) : { uid: 0, gid: 0 }
const { user, group } = personal
? identityNames(env)
: { user: 'root', group: 'root' }
return {
type: 'symlink',
path: abs,
size,
mode: S_IFLNK | 0o777,
uid,
gid,
user,
group,
mtimeMs: now,
ctimeMs: now,
nlink,
linkname
}
}
if (type === 'directory') {
const mode = personal
? S_IFDIR | (0o777 & ~parseUmask(env))
: S_IFDIR | 0o755
const { uid, gid } = personal ? parseUidGid(env) : { uid: 0, gid: 0 }
const { user, group } = personal
? identityNames(env)
: { user: 'root', group: 'root' }
return {
type: 'directory',
path: abs,
size: 0,
mode,
uid,
gid,
user,
group,
mtimeMs: now,
ctimeMs: now,
nlink
}
}
// file
let mode = S_IFREG | 0o644
if (!personal) {
if (abs.startsWith('/bin/') || abs.startsWith('/boot/'))
mode = S_IFREG | 0o555
else if (abs.startsWith('/etc/')) mode = S_IFREG | 0o644
else mode = S_IFREG | 0o644
return {
type: 'file',
path: abs,
size: 0,
mode,
uid: 0,
gid: 0,
user: 'root',
group: 'root',
mtimeMs: now,
ctimeMs: now,
nlink
}
}
const umask = parseUmask(env)
let fm = S_IFREG | (0o666 & ~umask)
if (entryHints.executable) fm |= 0o111
const { uid, gid } = parseUidGid(env)
const { user, group } = identityNames(env)
return {
type: 'file',
path: abs,
size: 0,
mode: fm,
uid,
gid,
user,
group,
mtimeMs: now,
ctimeMs: now,
nlink
}
}
/**
* @param {BareOsMeta} bo
* @param {'file' | 'directory' | 'symlink'} type
* @param {number} size
* @param {string} abs
* @param {string} [linkname]
*/
export function statFromBareOs(bo, type, size, abs, linkname) {
const o = {
type,
path: abs,
size,
mode: bo.mode,
uid: bo.uid,
gid: bo.gid,
user: bo.uname,
group: bo.gname,
mtimeMs: bo.mtimeMs,
ctimeMs: bo.ctimeMs,
nlink: 1
}
if (linkname != null) o.linkname = linkname
return o
}
/**
* @param {string} abs
*/
export function isVirtualMountPoint(abs) {
const n = abs.replace(/\/+$/, '') || '/'
return (
abs === '/' ||
abs === '/home' ||
abs === '/mnt' ||
abs === '/mirror' ||
abs === '/var' ||
abs === '/home/' ||
abs === '/mnt/' ||
abs === '/mirror/' ||
abs === '/var/' ||
n === '/proc' ||
n === '/proc/self' ||
n === '/sys' ||
n === '/sys/fs' ||
n === '/sys/fs/bare_os' ||
n === '/run' ||
n === '/run/bare-os' ||
n === '/dev'
)
}
/**
* @param {number} mode
* @param {'file' | 'directory' | 'symlink'} type
*/
export function formatModeString(mode, type) {
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
const perm = mode & 0o777
const r = (bit) => (perm & bit ? 'r' : '-')
const w = (bit) => (perm & bit ? 'w' : '-')
const x = (bit) => (perm & bit ? 'x' : '-')
return (
typeChar +
r(0o400) +
w(0o200) +
x(0o100) +
r(0o040) +
w(0o020) +
x(0o010) +
r(0o004) +
w(0o002) +
x(0o001)
)
}
/**
* @param {number} mtimeMs
* @param {number} [nowMs]
*/
export function formatLsMtime(mtimeMs, nowMs = Date.now()) {
const d = new Date(mtimeMs)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
const mon = months[d.getMonth()]
const day = String(d.getDate()).padStart(2, ' ')
const sixMo = 180 * 24 * 3600 * 1000
if (Math.abs(nowMs - mtimeMs) > sixMo) {
const yr = String(d.getFullYear()).padStart(4, ' ')
return `${mon} ${day} ${yr}`
}
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${mon} ${day} ${hh}:${mm}`
}
/**
* @param {number} size
*/
export function posixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
/**
* @param {{ uid: number, gid: number, mode: number }} st
* @param {number} euid
* @param {number} egid
* @param {'r' | 'w' | 'x'} op
* @param {'file' | 'directory'} kind
*/
export function modeAllows(st, euid, egid, op, kind) {
const mode = st.mode & 0o777
const bit = op === 'r' ? 0o444 : op === 'w' ? 0o222 : 0o111
let masked = 0
if (euid === st.uid) masked = (mode >> 6) & 7
else if (egid === st.gid) masked = (mode >> 3) & 7
else masked = mode & 7
const need = op === 'r' ? 4 : op === 'w' ? 2 : 1
return (masked & need) === need
}