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} 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} 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} 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} */ (value).metadata if (!md || typeof md !== 'object') return null const b = /** @type {Record} */ (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 | null | undefined} existingMetadata * @param {BareOsMeta} bareOs */ export function mergeEntryMetadata(existingMetadata, bareOs) { const base = existingMetadata && typeof existingMetadata === 'object' ? { ...existingMetadata } : {} return { ...base, bareOs } } /** * @param {Record} 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} env * @param {{ executable?: boolean, bumpMtime?: boolean, touchCtime?: boolean, legacyExecutable?: boolean }} [opts] * @returns {BareOsMeta} */ export function mergeBareOsOnWrite(prev, env, opts = {}) { const now = Date.now() 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 = S_IFREG | (0o666 & ~umask) if (wantEx) mode |= 0o111 return { mode, uid, gid, uname: user, gname: group, mtimeMs: now, ctimeMs: 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 } return { mode, uid: prev.uid, gid: prev.gid, uname: prev.uname, gname: prev.gname, mtimeMs: opts.bumpMtime !== false ? now : prev.mtimeMs, ctimeMs: opts.touchCtime ? now : prev.ctimeMs } } /** * @param {Record} 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} 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 === '/var' || abs === '/home/' || abs === '/mnt/' || 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 }