import fs from "fs";
import path from "path";
import { createGunzip } from "zlib";
import { Writable } from "stream";
import { pipeline } from "stream/promises";
import { hasSymlinkComponent } from "./rootfs.js";
const TAR_TYPE_FILE = 0x30;
const TAR_TYPE_DIRECTORY = 0x35;
const TAR_TYPE_SYMLINK = 0x32;
const TAR_TYPE_HARDLINK = 0x31;
const TAR_TYPE_PAX_LOCAL = 0x78;
const TAR_TYPE_PAX_GLOBAL = 0x67;
const TAR_TYPE_GNU_LONGNAME = 0x4c;
const TAR_TYPE_GNU_LONGLINK = 0x4b;
/** Parse a raw tar archive buffer into entries */
export function parseTar(buf) {
    const entries = [];
    let offset = 0;
    let globalPaxHeaders = {};
    let nextPaxHeaders = null;
    let nextLongName = null;
    let nextLongLink = null;
    while (offset + 512 <= buf.length) {
        const header = buf.subarray(offset, offset + 512);
        // Check for end-of-archive (two zero blocks)
        if (header.every((b) => b === 0)) {
            break;
        }
        const name = readTarString(header, 0, 100);
        const mode = parseInt(readTarString(header, 100, 8), 8) || 0;
        const size = parseInt(readTarString(header, 124, 12), 8) || 0;
        const typeFlag = header[156];
        const headerLinkName = readTarString(header, 157, 100);
        // Handle UStar prefix
        const magic = readTarString(header, 257, 6);
        let fullName = name;
        if (magic === "ustar" || magic === "ustar\0") {
            const prefix = readTarString(header, 345, 155);
            if (prefix) {
                fullName = `${prefix}/${name}`;
            }
        }
        offset += 512;
        let content = null;
        if (size > 0) {
            content = Buffer.from(buf.subarray(offset, offset + size));
            offset += Math.ceil(size / 512) * 512;
        }
        // PAX extended headers (type 'x' or 'g')
        if (typeFlag === TAR_TYPE_PAX_LOCAL || typeFlag === TAR_TYPE_PAX_GLOBAL) {
            const paxHeaders = parsePaxHeaders(content);
            if (typeFlag === TAR_TYPE_PAX_GLOBAL) {
                globalPaxHeaders = {
                    ...globalPaxHeaders,
                    ...paxHeaders,
                };
            }
            else {
                nextPaxHeaders = paxHeaders;
            }
            continue;
        }
        // GNU long path/link name records apply to the next non-meta entry
        if (typeFlag === TAR_TYPE_GNU_LONGNAME) {
            nextLongName = readLongTarString(content);
            continue;
        }
        if (typeFlag === TAR_TYPE_GNU_LONGLINK) {
            nextLongLink = readLongTarString(content);
            continue;
        }
        const effectivePaxHeaders = nextPaxHeaders
            ? { ...globalPaxHeaders, ...nextPaxHeaders }
            : globalPaxHeaders;
        if (nextLongName) {
            fullName = nextLongName;
        }
        else if (effectivePaxHeaders.path) {
            fullName = effectivePaxHeaders.path;
        }
        let linkName = headerLinkName;
        if (nextLongLink) {
            linkName = nextLongLink;
        }
        else if (effectivePaxHeaders.linkpath) {
            linkName = effectivePaxHeaders.linkpath;
        }
        const type = typeFlag === 0 || typeFlag === TAR_TYPE_FILE
            ? 0 // regular file
            : typeFlag === TAR_TYPE_DIRECTORY
                ? 5 // directory
                : typeFlag === TAR_TYPE_SYMLINK
                    ? 2 // symlink
                    : typeFlag === TAR_TYPE_HARDLINK
                        ? 1 // hardlink
                        : typeFlag;
        if (type === 0 && content === null) {
            content = Buffer.alloc(0);
        }
        entries.push({ name: fullName, type, mode, size, linkName, content });
        nextPaxHeaders = null;
        nextLongName = null;
        nextLongLink = null;
    }
    return entries;
}
function parsePaxHeaders(content) {
    if (!content || content.length === 0) {
        return {};
    }
    const out = {};
    let offset = 0;
    while (offset < content.length) {
        const spaceIdx = content.indexOf(0x20, offset);
        if (spaceIdx === -1) {
            break;
        }
        const lenStr = content.subarray(offset, spaceIdx).toString("utf8").trim();
        const recordLen = Number.parseInt(lenStr, 10);
        if (!Number.isFinite(recordLen) || recordLen <= 0) {
            break;
        }
        const recordEnd = offset + recordLen;
        if (recordEnd > content.length) {
            break;
        }
        const record = content.subarray(spaceIdx + 1, recordEnd).toString("utf8");
        const newline = record.endsWith("\n") ? record.slice(0, -1) : record;
        const eqIdx = newline.indexOf("=");
        if (eqIdx !== -1) {
            const key = newline.slice(0, eqIdx);
            const value = newline.slice(eqIdx + 1);
            if (key) {
                out[key] = value;
            }
        }
        offset = recordEnd;
    }
    return out;
}
function readLongTarString(content) {
    if (!content || content.length === 0) {
        return "";
    }
    let end = content.length;
    while (end > 0 && (content[end - 1] === 0 || content[end - 1] === 0x0a)) {
        end -= 1;
    }
    return content.subarray(0, end).toString("utf8");
}
function readTarString(buf, offset, length) {
    const slice = buf.subarray(offset, offset + length);
    const nullIdx = slice.indexOf(0);
    const end = nullIdx === -1 ? length : nullIdx;
    return slice.subarray(0, end).toString("utf8");
}
/** Decompress a .tar.gz file and return the raw tar buffer */
export async function decompressTarGz(filePath) {
    const chunks = [];
    const input = fs.createReadStream(filePath);
    const gunzip = createGunzip();
    const collector = new Writable({
        write(chunk, _encoding, cb) {
            chunks.push(chunk);
            cb();
        },
    });
    await pipeline(input, gunzip, collector);
    return Buffer.concat(chunks);
}
/** Extract a .tar.gz file into a directory (safe against symlink traversal) */
export async function extractTarGz(tarGzPath, destDir) {
    const raw = await decompressTarGz(tarGzPath);
    const entries = parseTar(raw);
    extractEntries(entries, destDir);
}
/** Extract tar entries into a directory with symlink-safety checks */
export function extractEntries(entries, destDir) {
    const absRoot = path.resolve(destDir);
    for (const entry of entries) {
        // Skip APK metadata files
        if (entry.name.startsWith(".") && !entry.name.startsWith("./")) {
            continue;
        }
        const target = path.resolve(destDir, entry.name);
        // Guard: target must be inside destDir
        if (!target.startsWith(absRoot + path.sep) && target !== absRoot) {
            continue;
        }
        // Guard: no symlink in any intermediate path component
        if (hasSymlinkComponent(target, absRoot)) {
            process.stderr.write(`skipping symlinked path ${entry.name}\n`);
            continue;
        }
        // Prepare for extraction — remove existing entry if needed
        prepareTarget(target, entry.type === 5);
        if (entry.type === 5) {
            // Directory
            fs.mkdirSync(target, { recursive: true });
        }
        else if (entry.type === 2) {
            // Symlink
            fs.mkdirSync(path.dirname(target), { recursive: true });
            try {
                fs.symlinkSync(entry.linkName, target);
            }
            catch (err) {
                if (err.code !== "EEXIST")
                    throw err;
            }
        }
        else if (entry.type === 1) {
            // Hardlink
            const linkTarget = path.resolve(destDir, entry.linkName);
            if (linkTarget.startsWith(absRoot + path.sep) &&
                fs.existsSync(linkTarget)) {
                fs.mkdirSync(path.dirname(target), { recursive: true });
                try {
                    fs.linkSync(linkTarget, target);
                }
                catch {
                    // Fall back to copy
                    if (fs.existsSync(linkTarget)) {
                        fs.copyFileSync(linkTarget, target);
                    }
                }
            }
        }
        else if (entry.type === 0 && entry.content) {
            // Regular file
            fs.mkdirSync(path.dirname(target), { recursive: true });
            fs.writeFileSync(target, entry.content);
            try {
                fs.chmodSync(target, entry.mode & 0o7777);
            }
            catch {
                // chmod may fail on some platforms; ignore
            }
        }
    }
}
/** Prepare a target path for extraction: remove existing entries that conflict */
function prepareTarget(target, isDir) {
    let stat;
    try {
        stat = fs.lstatSync(target);
    }
    catch {
        return; // Doesn't exist, nothing to do
    }
    if (isDir && stat.isDirectory())
        return; // Already a directory, fine
    try {
        if (stat.isDirectory()) {
            fs.rmSync(target, { recursive: true, force: true });
        }
        else {
            fs.unlinkSync(target);
        }
    }
    catch {
        // Try harder: fix permissions then remove
        try {
            fs.chmodSync(target, 0o700);
        }
        catch {
            // ignore
        }
        try {
            if (stat.isDirectory()) {
                fs.rmSync(target, { recursive: true, force: true });
            }
            else {
                fs.unlinkSync(target);
            }
        }
        catch {
            // Last resort: ignore
        }
    }
}
//# sourceMappingURL=tar.js.map