422 lines
9.9 KiB
JavaScript
422 lines
9.9 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 }} [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<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 === '/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
|
|
}
|