Files
bare-operating-system/packages/bare-os-booter/lib/tar-cli.js
T
Raven Scott b795397261 Added consistent non-empty version output for:
curl --version in packages/bare-os-coreutils/src/curl.js
wget --version in packages/bare-os-coreutils/src/wget.js
openssl version and openssl --version in packages/bare-os-coreutils/src/openssl.js
nano --version (via edit) in packages/bare-os-coreutils/src/edit.js
Updated tar compatibility in packages/bare-os-booter/lib/tar-cli.js:

Added tar --version
Added grouped short-flag parsing (-czf, etc.)
Added first-arg legacy cluster parsing (czf)
Added - archive target support for create/list/extract stream paths
Kept implementation as ustar subset (accepts z for compatibility parsing)
Fixed pipeline/timeout behavior:

Optimized head line mode to stop scanning early (no full split) in packages/bare-os-coreutils/src/head.js
Added bounded safety for yes cap in packages/bare-os-coreutils/src/yes.js
Hardened timeout so timed-out commands reliably produce exit code 124 in packages/bare-os-coreutils/src/timeout.js
Added scoped expr compatibility in shell:

Prevented pathname expansion of bare * only for expr arithmetic-token case in packages/bare-os-booter/lib/shell.js
Added glob option plumbing in packages/bare-os-booter/lib/shell-glob.js
2026-04-27 08:07:18 -04:00

451 lines
12 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
}
if (args.includes('--version')) {
ctx.console.log('tar (Bare OS ustar subset) 0.1')
return
}
let create = false
let list = false
let extract = false
let sawCluster = false
/** @type {string[]} */
const rest = []
const consumeCluster = (cluster, nextArg) => {
let mode = ''
let takesArchive = false
for (const c of cluster) {
if (c === 'c' || c === 't' || c === 'x') {
if (mode && mode !== c) return { ok: false, reason: 'conflicting mode flags' }
mode = c
continue
}
if (c === 'f') {
takesArchive = true
continue
}
if (c === 'z') {
// Accepted for compatibility with common tar forms; current output remains ustar.
continue
}
return { ok: false, reason: 'unsupported flag -' + c }
}
if (!mode) return { ok: false, reason: 'missing mode flag' }
if (mode === 'c') create = true
if (mode === 't') list = true
if (mode === 'x') extract = true
if (takesArchive) rest.push(nextArg || '')
return { ok: true, consumedArchive: takesArchive }
}
for (let i = 0; i < args.length; i++) {
const a = args[i]
if (!sawCluster && i === 0 && /^[ctxfz]+$/.test(a)) {
const parsed = consumeCluster(a, args[i + 1])
if (!parsed.ok) {
ctx.console.error('tar: ' + parsed.reason)
ctx.exitCode = 1
return
}
sawCluster = true
if (parsed.consumedArchive) i++
continue
}
if (/^-[ctxfz]+$/.test(a)) {
const parsed = consumeCluster(a.slice(1), args[i + 1])
if (!parsed.ok) {
ctx.console.error('tar: ' + parsed.reason)
ctx.exitCode = 1
return
}
sawCluster = true
if (parsed.consumedArchive) i++
continue
}
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
}
if (archive === '-') {
if (typeof ctx.bareOsBinWrite === 'function') {
ctx.bareOsBinWrite(out)
} else if (
globalThis.process &&
globalThis.process.stdout &&
typeof globalThis.process.stdout.write === 'function'
) {
globalThis.process.stdout.write(out)
} else {
ctx.console.error('tar: stdout is unavailable for archive stream')
ctx.exitCode = 1
}
} else {
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 raw
if (archive === '-') {
const stdin = typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
raw = b4.from(stdin, 'binary')
} else {
let absA
try {
absA = vfs.resolveLogical(String(archive))
} catch {
ctx.console.error('tar: bad archive path')
ctx.exitCode = 1
return
}
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
}