Files
bare-operating-system/packages/bare-os-booter/lib/tar-cli.js
T
Raven Scott ae6f1cf8ac Implement the 20-task Bare OS POSIX + P2P roadmap: holepunch lockfile drift
reporting and audit:holepunch-clones docs; placeholder baseline automation;
maintainer kernel-image sync script; protomux schema coupling in protocol
tests; disk.os RPC hints (replication_operator_sketch, hyperblobs/blind v3);
union/mirror VFS + warm-cache selective eviction tests; extension resolver
coverage; curl/wget fall through PATH when BARE_OS_DELEGATE_ALLOW excludes
delegates (kernel-runner) with handbook/ctx docs; socket contract / Wasm /
boot budget strict path / hrpc allowlist tests; subprocess bridge meta;
shell until gate + tar stat metadata; POSIX profile triplet pretest verifier;
regenerate kernel bundle, seeder parity, and audit artifacts as needed.

Covers KERNEL_CONTRACT, POSIX_DECLARED_PROFILE, handbook, developer-guide,
scripts/README, and related reference docs.
2026-04-05 02:21:07 -04:00

378 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Minimal POSIX ustar tar for VFS paths: create / list / extract.
* Supported:
* tar -cf ARCHIVE PATH [PATH...]
* tar -tf ARCHIVE
* tar -xf ARCHIVE
* GNU extensions / compression are not supported.
* Create path: when **`vfs.stat`** exists, **`mtime`** / **`mode`** reflect the source file (Issue 7style metadata).
*/
import b4a from 'b4a'
import unixPathResolve from 'unix-path-resolve'
const BLK = 512
function utf8Encode(str) {
return b4a.from(String(str), 'utf8')
}
function utf8Decode(buf) {
return b4a.toString(buf, 'utf8')
}
/**
* @param {Uint8Array} buf
* @param {number} off
* @param {number} len
*/
function parseOctalField(buf, off, len) {
let s = ''
for (let i = 0; i < len; i++) {
const c = buf[off + i]
if (c === 0 || c === 32) break
s += String.fromCharCode(c)
}
const n = Number.parseInt(s.trim(), 8)
return Number.isFinite(n) ? n : 0
}
/**
* @param {string} oct
* @param {number} len
*/
function octalPad(oct, len) {
const o = oct + '\0'
const b = new Uint8Array(len)
const enc = utf8Encode(o)
b.set(enc.slice(0, len - 1))
return b
}
/**
* @param {Record<string, unknown>} header
*/
export function encodeUstarHeader(header) {
const h = new Uint8Array(BLK)
const name = String(header.name || '').slice(0, 100)
const mode = String(header.mode || 0o644).slice(0, 7)
const uid = String(header.uid || 0).slice(0, 7)
const gid = String(header.gid || 0).slice(0, 7)
const size = String(header.size || 0).slice(0, 11)
const mtime = String(header.mtime || 0).slice(0, 11)
const typeflag = header.typeflag || '0'
const linkname = String(header.linkname || '').slice(0, 100)
const w = (str, off, max) => {
const enc = utf8Encode(str)
h.set(enc.slice(0, max), off)
}
w(name, 0, 100)
h.set(octalPad(mode, 8), 100)
h.set(octalPad(uid, 8), 108)
h.set(octalPad(gid, 8), 116)
h.set(octalPad(size, 12), 124)
h.set(octalPad(mtime, 12), 136)
// ustar checksum field (sum of header bytes; standard layout)
for (let i = 148; i < 156; i++) h[i] = 32
h[156] = typeflag.charCodeAt(0)
w(linkname, 157, 100)
w('ustar\0', 257, 6)
w('00', 263, 2)
let sum = 0
for (let i = 0; i < BLK; i++) {
if (i >= 148 && i < 156) sum += 32
else sum += h[i]
}
const cks = octalPad(sum.toString(8), 8)
h.set(cks, 148)
return h
}
/**
* @param {Uint8Array} block
*/
function parseUstarBlock(block) {
const name = utf8Decode(block.subarray(0, 100)).replace(/\0.*$/, '')
const size = parseOctalField(block, 124, 12)
const typeflag = String.fromCharCode(block[156] || 48)
const linkname = utf8Decode(block.subarray(157, 257)).replace(/\0.*$/, '')
return { name, size, typeflag, linkname }
}
/**
* Resolve ustar hard-link `linkname` relative to the directory of the new link path.
* @param {string} cwdAbs
* @param {string} entryName
* @param {string} linkname
*/
function resolveTarHardlinkSource(cwdAbs, entryName, linkname) {
const ln = String(linkname).trim()
if (!ln) return ''
if (ln.startsWith('/')) return ln.replace(/\/+$/, '') || '/'
const dest =
cwdAbs === '/' ? '/' + entryName : cwdAbs.replace(/\/+$/, '') + '/' + entryName
const slash = dest.lastIndexOf('/')
const destDir = slash <= 0 ? '/' : dest.slice(0, slash)
const rel = ln.replace(/^\.\//, '')
return unixPathResolve(destDir === '/' ? '/' : destDir + '/', rel)
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
*/
export async function runTarCli(ctx, argv) {
const args = argv.slice(1).filter((a) => a !== '--')
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: tar -cf ARCHIVE PATH...\n' +
' tar -tf ARCHIVE\n' +
' tar -xf ARCHIVE\n' +
'Bare OS: ustar only, VFS paths. Hard links: copy-on-extract unless BARE_OS_VFS_STRICT_HARDLINK.\n'
)
return
}
let create = false
let list = false
let extract = false
/** @type {string[]} */
const rest = []
for (let i = 0; i < args.length; i++) {
const a = args[i]
if (a === '-cf' || a === '-fc') {
create = true
rest.push(args[++i] || '')
continue
}
if (a === '-tf' || a === '-ft') {
list = true
rest.push(args[++i] || '')
continue
}
if (a === '-xf' || a === '-fx') {
extract = true
rest.push(args[++i] || '')
continue
}
if (a.startsWith('-')) {
ctx.console.error('tar: unsupported flag ' + a)
ctx.exitCode = 1
return
}
rest.push(a)
}
const vfs = ctx.vfs
const b4 = ctx.b4a
if (!vfs || !b4 || typeof vfs.readFile !== 'function') {
ctx.console.error('tar: vfs unavailable')
ctx.exitCode = 1
return
}
if (create) {
const archive = rest.shift()
if (!archive || rest.length === 0) {
ctx.console.error('tar: -cf needs archive and at least one path')
ctx.exitCode = 1
return
}
/** @type {Uint8Array[]} */
const parts = []
for (const p of rest) {
let abs
try {
abs = vfs.resolveLogical(String(p))
} catch {
ctx.console.error('tar: bad path ' + p)
ctx.exitCode = 1
return
}
let data
try {
data = await vfs.readFile(abs)
} catch (e) {
ctx.console.error(
'tar: ' + ((e && /** @type {Error} */ (e).message) || String(e))
)
ctx.exitCode = 1
return
}
const u8 = b4a.isBuffer(data) ? new Uint8Array(data) : new Uint8Array(data)
const base = abs.includes('/') ? abs.split('/').pop() : abs
const name = base || 'file'
if (name.length > 99) {
ctx.console.error('tar: path name too long for ustar: ' + name)
ctx.exitCode = 1
return
}
let mode = 0o644
let mtime = Math.floor(Date.now() / 1000)
if (typeof vfs.stat === 'function') {
try {
const st = await vfs.stat(abs)
if (st && typeof st === 'object') {
if (typeof st.mtimeMs === 'number' && Number.isFinite(st.mtimeMs)) {
mtime = Math.max(0, Math.floor(st.mtimeMs / 1000))
}
if (typeof st.mode === 'number' && Number.isFinite(st.mode)) {
mode = st.mode & 0o7777
}
}
} catch {
/* keep defaults */
}
}
const hdr = encodeUstarHeader({
name,
size: u8.length,
mode,
mtime
})
parts.push(hdr)
parts.push(u8)
const pad = (BLK - (u8.length % BLK)) % BLK
if (pad) parts.push(new Uint8Array(pad))
}
parts.push(new Uint8Array(BLK * 2))
let total = 0
for (const q of parts) total += q.length
const out = new Uint8Array(total)
let o = 0
for (const q of parts) {
out.set(q, o)
o += q.length
}
let absA
try {
absA = vfs.resolveLogical(String(archive))
} catch {
ctx.console.error('tar: bad archive path')
ctx.exitCode = 1
return
}
try {
await vfs.writeFile(absA, b4.from(out))
} catch (e) {
ctx.console.error(
'tar: ' + ((e && /** @type {Error} */ (e).message) || String(e))
)
ctx.exitCode = 1
}
return
}
if (list || extract) {
const archive = rest[0]
if (!archive) {
ctx.console.error('tar: missing archive')
ctx.exitCode = 1
return
}
let absA
try {
absA = vfs.resolveLogical(String(archive))
} catch {
ctx.console.error('tar: bad archive path')
ctx.exitCode = 1
return
}
let raw
try {
raw = await vfs.readFile(absA)
} catch (e) {
ctx.console.error(
'tar: ' + ((e && /** @type {Error} */ (e).message) || String(e))
)
ctx.exitCode = 1
return
}
const buf = b4a.isBuffer(raw) ? new Uint8Array(raw) : new Uint8Array(raw)
let off = 0
while (off + BLK <= buf.length) {
const block = buf.subarray(off, off + BLK)
if (block.every((b) => b === 0)) break
off += BLK
const rec = parseUstarBlock(block)
const { name, size, typeflag, linkname } = rec
if (off + size > buf.length) {
ctx.console.error('tar: corrupt archive (short read)')
ctx.exitCode = 1
return
}
const body = buf.subarray(off, off + size)
off += size
off += (BLK - (size % BLK)) % BLK
if (!name) continue
if (typeflag === '1') {
if (list) ctx.console.log(name)
if (extract) {
const strict =
vfs.env &&
(vfs.env.BARE_OS_VFS_STRICT_HARDLINK === '1' ||
vfs.env.BARE_OS_VFS_STRICT_HARDLINK === 'true')
if (strict) {
ctx.console.error(
'tar: hard link entries not supported with BARE_OS_VFS_STRICT_HARDLINK (unset for copy-on-extract)'
)
ctx.exitCode = 1
return
}
const cwd = vfs.resolveLogical('.')
const target =
cwd === '/' ? '/' + name : cwd.replace(/\/$/, '') + '/' + name
const src = resolveTarHardlinkSource(cwd, name, linkname)
if (!linkname.trim() || !src) {
ctx.console.error('tar: hard link missing linkname: ' + name)
ctx.exitCode = 1
return
}
try {
const data = await vfs.readFile(src)
await vfs.writeFile(target, b4.from(data))
} catch (e) {
ctx.console.error(
'tar: extract ' +
name +
': ' +
((e && /** @type {Error} */ (e).message) || String(e))
)
ctx.exitCode = 1
return
}
}
continue
}
if (typeflag === '0' || typeflag === '\0' || typeflag === '') {
if (list) ctx.console.log(name)
if (extract) {
const cwd = vfs.resolveLogical('.')
const target =
cwd === '/' ? '/' + name : cwd.replace(/\/$/, '') + '/' + name
try {
await vfs.writeFile(target, b4.from(body))
} catch (e) {
ctx.console.error(
'tar: extract ' +
name +
': ' +
((e && /** @type {Error} */ (e).message) || String(e))
)
ctx.exitCode = 1
return
}
}
}
}
return
}
ctx.console.error('tar: specify -cf, -tf, or -xf')
ctx.exitCode = 1
}