/** * 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. */ import b4a from 'b4a' 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} header */ 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 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('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) return { name, size, typeflag } } /** * @param {Record} 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.\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 } const hdr = encodeUstarHeader({ name, size: u8.length, mode: 0o644, mtime: Math.floor(Date.now() / 1000) }) 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 } = 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 === '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 }