/* p_ps1.cpp --

   This file is part of the UPX executable compressor.

   Copyright (C) Markus Franz Xaver Johannes Oberhumer
   Copyright (C) Laszlo Molnar
   Copyright (C) Jens Medoch
   All Rights Reserved.

   UPX and the UCL library are free software; you can redistribute them
   and/or modify them under the terms of the GNU General Public License as
   published by the Free Software Foundation; either version 2 of
   the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; see the file COPYING.
   If not, write to the Free Software Foundation, Inc.,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   Markus F.X.J. Oberhumer              Laszlo Molnar
   <markus@oberhumer.com>               <ezerotven+github@gmail.com>

   Jens Medoch
   <jssg@users.sourceforge.net>
 */

#include "conf.h"
#include "file.h"
#include "filter.h"
#include "packer.h"
#include "p_ps1.h"
#define WANT_EHDR_ENUM 1
#include "p_elf_enum.h"
#include "linker.h"

static const CLANG_FORMAT_DUMMY_STATEMENT
#include "stub/mipsel.r3000-ps1.h"

#define CD_SEC        2048
#define PS_HDR_SIZE   CD_SEC
#define PS_RAM_SIZE   ram_size
#define PS_MIN_SIZE   (PS_HDR_SIZE * 3)
#define PS_MAX_SIZE   ((PS_RAM_SIZE * 95) / 100)
#define PS_STACK_SIZE (PS_RAM_SIZE / 256)

#define SZ_IH_BKUP  (10 * sizeof(LE32))
#define HD_CODE_OFS (sizeof(ps1_exe_t) + sz_cbh)

#define K0_BS    (0x80000000)
#define K1_BS    (0xa0000000)
#define EXE_BS   (ih.epc & K0_BS)
#define FIX_PSVR ((K1_BS - EXE_BS) + (PS_HDR_SIZE - HD_CODE_OFS))

// lui / addiu
#define MIPS_HI(a)   (((a) >> 16) + (((a) &0x8000) >> 15))
#define MIPS_LO(a)   ((a) &0xffff)
#define MIPS_PC16(a) ((a) >> 2)
#define MIPS_PC26(a) (((a) &0x0fffffff) >> 2)

/*************************************************************************
// ps1 exe looks like this:
// 1. <header>  2048 bytes
// 2. <body>    plain binary
//
// header:  contains the ps1_exe_t structure 188 bytes at offset zero
//          rest is filled with zeros to reach the required
//          cd mode 2 data sector size of 2048 bytes
// body:    contains the binary data / code of the executable
//          reqiures: executable code must be aligned to 4
//                    must be aligned to 2048 to run from a CD
//          optional: not aligned to 2048 (for console run only)
**************************************************************************/

PackPs1::PackPs1(InputFile *f)
    : super(f), isCon(!opt->ps1_exe.boot_only), is32Bit(!opt->ps1_exe.do_8bit) {
    bele = &N_BELE_RTP::le_policy;

    COMPILE_TIME_ASSERT(sizeof(ps1_exe_t) == 136)
    COMPILE_TIME_ASSERT(sizeof(ps1_exe_hb_t) == 44)
    COMPILE_TIME_ASSERT(sizeof(ps1_exe_chb_t) == 5)
    COMPILE_TIME_ASSERT_ALIGNED1(ps1_exe_t)
    COMPILE_TIME_ASSERT_ALIGNED1(ps1_exe_hb_t)
    COMPILE_TIME_ASSERT_ALIGNED1(ps1_exe_chb_t)

    COMPILE_TIME_ASSERT(PS_HDR_SIZE > sizeof(ps1_exe_t))
    COMPILE_TIME_ASSERT(SZ_IH_BKUP == 40)

    fdata_size = file_size - PS_HDR_SIZE;
    ram_size = !opt->ps1_exe.do_8mib ? 0x200000 : 0x800000;
}

const int *PackPs1::getCompressionMethods(int method, int level) const {
    if (is32Bit)
        return Packer::getDefaultCompressionMethods_le32(method, level);
    else
        return Packer::getDefaultCompressionMethods_8(method, level);
}

const int *PackPs1::getFilters() const { return nullptr; }

Linker *PackPs1::newLinker() const { return new ElfLinkerMipsLE; }

/*************************************************************************
// util
//   readFileHeader() reads ih and checks for illegal values
//   checkFileHeader() checks ih for legal but unsupported values
**************************************************************************/

int PackPs1::readFileHeader() {
    fi->seek(0, SEEK_SET);
    fi->readx(&ih, sizeof(ih));
    if (memcmp(&ih.id[0], "PS-X EXE", 8) != 0 && memcmp(&ih.id[0], "EXE X-SP", 8) != 0)
        return 0;
    if (ih.text != 0 || ih.data != 0)
        return 0;
    return UPX_F_PS1_EXE;
}

bool PackPs1::readBkupHeader() {
    fi->seek(sizeof(ps1_exe_t) + 8, SEEK_SET);
    fi->readx(&bh, sizeof(bh));

    if (bh.ih_csum != upx_adler32(&bh, SZ_IH_BKUP)) {
        byte buf[sizeof(bh)];
        fi->seek(sizeof(ps1_exe_t), SEEK_SET);
        fi->readx(buf, sizeof(bh));
        if (!getBkupHeader(buf, (byte *) &bh))
            return false;
    }
    return true;
}

#define INIT_BH_BKUP(p, l)                                                                         \
    do {                                                                                           \
        (p)->id = '1';                                                                             \
        (p)->len = l;                                                                              \
    } while (0)
#define ADLER16(a) (((a) >> 16) ^ ((a) &0xffff))

void PackPs1::putBkupHeader(const byte *src, byte *dst, unsigned *len) {
    unsigned sz_cbh = MemBuffer::getSizeForCompression(SZ_IH_BKUP);

    if (src && dst) {
        ByteArray(cpr_bh, sz_cbh);

        memset(cpr_bh, 0, sizeof(bh));
        ps1_exe_chb_t *p = (ps1_exe_chb_t *) cpr_bh;

        int r = upx_compress(src, SZ_IH_BKUP, &p->ih_bkup, &sz_cbh, nullptr, M_NRV2E_8, 10, nullptr,
                             nullptr);
        if (r != UPX_E_OK || sz_cbh >= SZ_IH_BKUP)
            throwInternalError("header compression failed");
        INIT_BH_BKUP(p, sz_cbh);
        *len = ALIGN_UP(sz_cbh + usizeof(ps1_exe_chb_t) - 1, 4u);
        p->ih_csum = ADLER16(upx_adler32(&ih.epc, SZ_IH_BKUP));
        memcpy(dst, cpr_bh, SZ_IH_BKUP);
    } else
        throwInternalError("header compression failed");
}

#define ADLER16_HI(a, b) ((((a) &0xffff) ^ (b)) << 16)
#define ADLER16_LO(a, b) (((a) >> 16) ^ (b))
#define RE_ADLER16(a, b) (ADLER16_HI(a, b) | ADLER16_LO(a, b))

bool PackPs1::getBkupHeader(byte *p, byte *dst) {
    ps1_exe_chb_t *src = (ps1_exe_chb_t *) p;

    if (src && (src->id == '1' && src->len < SZ_IH_BKUP) && dst) {
        ByteArray(unc_bh, MemBuffer::getSizeForDecompression(SZ_IH_BKUP));

        unsigned sz_bh = SZ_IH_BKUP;
        int r = upx_decompress((const byte *) &src->ih_bkup, src->len, unc_bh, &sz_bh, M_NRV2E_8,
                               nullptr);
        if (r == UPX_E_OUT_OF_MEMORY)
            throwOutOfMemoryException();
        if (r != UPX_E_OK || sz_bh != SZ_IH_BKUP)
            throwInternalError("header decompression failed");
        unsigned ad = upx_adler32(unc_bh, SZ_IH_BKUP);
        unsigned ch = src->ih_csum;
        if (ad != RE_ADLER16(ad, ch))
            throwInternalError("backup header damaged");
        memcpy(dst, unc_bh, SZ_IH_BKUP);
    } else
        return false;
    return true;
}

bool PackPs1::checkFileHeader() {
    if (fdata_size != ih.tx_len || (ih.tx_len & 3)) {
        if (!opt->force)
            throwCantPack("file size entry damaged (try --force)");
        else {
            opt->info_mode += !opt->info_mode ? 1 : 0;
            infoWarning("fixing damaged header, keeping backup file");
            opt->backup = 1;
            ih.tx_len = fdata_size;
        }
    }
    if (!opt->force && (ih.da_ptr != 0 || ih.da_len != 0 || ih.bs_ptr != 0 || ih.bs_len != 0)) {
        infoWarning("unsupported header field entry");
        return false;
    }
    if (ih.is_ptr < (EXE_BS | (PS_RAM_SIZE - PS_STACK_SIZE))) {
        if (!opt->force)
            return false;
        else
            infoWarning("%s: stack pointer offset low", fi->getName());
    }
    return true;
}

/*************************************************************************
//
**************************************************************************/

tribool PackPs1::canPack() {
    byte buf[PS_HDR_SIZE - sizeof(ps1_exe_t)];

    if (!readFileHeader())
        return false;

    fi->readx(buf, sizeof(buf));
    checkAlreadyPacked(buf, sizeof(buf));

    for (size_t i = 0; i < sizeof(buf); i++)
        if (buf[i] != 0) {
            if (!opt->force)
                throwCantPack("unknown data in header (try --force)");
            else {
                opt->info_mode += !opt->info_mode ? 1 : 0;
                infoWarning("clearing header, keeping backup file");
                opt->backup = 1;
                break;
            }
        }
    if (!checkFileHeader())
        throwCantPack("unsupported header flags (try --force)");
    if (file_size < PS_MIN_SIZE)
        throwCantPack("file is too small");
    if (!opt->force && file_size_u > PS_MAX_SIZE)
        throwCantPack("file is too big (try --force)");
    return true;
}

/*************************************************************************
//
**************************************************************************/

void PackPs1::buildLoader(const Filter *) {
    const char *method = nullptr;

    if (ph.method == M_NRV2B_8)
        method = isCon ? "nrv2b.small,8bit.sub,nrv.done" : "nrv2b.8bit,nrv.done";
    else if (ph.method == M_NRV2D_8)
        method = isCon ? "nrv2d.small,8bit.sub,nrv.done" : "nrv2d.8bit,nrv.done";
    else if (ph.method == M_NRV2E_8)
        method = isCon ? "nrv2e.small,8bit.sub,nrv.done" : "nrv2e.8bit,nrv.done";
    else if (ph.method == M_NRV2B_LE32)
        method = isCon ? "nrv2b.small,32bit.sub,nrv.done" : "nrv2b.32bit,nrv.done";
    else if (ph.method == M_NRV2D_LE32)
        method = isCon ? "nrv2d.small,32bit.sub,nrv.done" : "nrv2d.32bit,nrv.done";
    else if (ph.method == M_NRV2E_LE32)
        method = isCon ? "nrv2e.small,32bit.sub,nrv.done" : "nrv2e.32bit,nrv.done";
    else if (M_IS_LZMA(ph.method))
        method = "nrv2b.small,8bit.sub,nrv.done,lzma.prep";
    else
        throwInternalError("unknown compression method");

    unsigned sa_tmp = sa_cnt;
    if (ph.overlap_overhead > sa_cnt) {
        if (!opt->force) {
            infoWarning("not in-place decompressible");
            throwCantPack("packed data overlap (try --force)");
        } else
            sa_tmp += overlap = ALIGN_UP((ph.overlap_overhead - sa_tmp), 4u);
    }

    if (isCon || M_IS_LZMA(ph.method))
        foundBss = findBssSection();

    if (M_IS_LZMA(ph.method) && !buildPart2) {
        initLoader(EM_MIPS_RS3_LE, stub_mipsel_r3000_ps1, sizeof(stub_mipsel_r3000_ps1));
        addLoader("decompressor.start", isCon ? "LZMA_DEC20" : "LZMA_DEC10", "lzma.init", nullptr);
        addLoader(sa_tmp > (0x10000 << 2) ? "memset.long" : "memset.short",
                  !foundBss ? "con.exit" : "bss.exit", nullptr);
    } else {
        if (M_IS_LZMA(ph.method) && buildPart2) {
            sz_lcpr = MemBuffer::getSizeForCompression(sz_lunc);
            mb_cprLoader.dealloc();
            mb_cprLoader.alloc(sz_lcpr);
            int r = upx_compress(getLoader(), sz_lunc, mb_cprLoader, &sz_lcpr, nullptr, M_NRV2B_8,
                                 10, nullptr, nullptr);
            if (r != UPX_E_OK || sz_lcpr >= sz_lunc)
                throwInternalError("loader compression failed");
            initLoader(EM_MIPS_RS3_LE, stub_mipsel_r3000_ps1, sizeof(stub_mipsel_r3000_ps1),
                       isCon || !M_IS_LZMA(ph.method) ? 0 : 1);
            linker->addSection("lzma.exec", mb_cprLoader, sz_lcpr, 0);
        } else
            initLoader(EM_MIPS_RS3_LE, stub_mipsel_r3000_ps1, sizeof(stub_mipsel_r3000_ps1));

        pad_code = ALIGN_UP_GAP((ph.c_len + (isCon ? sz_lcpr : 0)), 4u);
        assert(pad_code < 4);
        static const byte pad_buffer[4] = {0, 0, 0, 0};
        linker->addSection("pad.code", pad_buffer, pad_code, 0);

        if (isCon) {
            if (M_IS_LZMA(ph.method))
                addLoader(!foundBss ? "con.start" : "bss.con.start", method,
                          ih.tx_ptr & 0xffff ? "dec.ptr" : "dec.ptr.hi", "con.entry", "pad.code",
                          "lzma.exec", nullptr);
            else
                addLoader(!foundBss ? "con.start" : "bss.con.start", "con.mcpy",
                          ph.c_len & 3 ? "con.padcd" : "",
                          ih.tx_ptr & 0xffff ? "dec.ptr" : "dec.ptr.hi", "con.entry", method,
                          sa_cnt ? sa_cnt > (0x10000 << 2) ? "memset.long" : "memset.short" : "",
                          !foundBss ? "con.exit" : "bss.exit", "pad.code", nullptr);
        } else {
            if (M_IS_LZMA(ph.method))
                addLoader(!foundBss ? "cdb.start.lzma" : "bss.cdb.start.lzma", "pad.code",
                          !foundBss ? "cdb.entry.lzma" : "bss.cdb.entry.lzma", method,
                          "cdb.lzma.cpr", ih.tx_ptr & 0xffff ? "dec.ptr" : "dec.ptr.hi",
                          "lzma.exec", nullptr);
            else {
                assert(foundBss != true);
                addLoader("cdb.start", "pad.code", "cdb.entry",
                          ih.tx_ptr & 0xffff ? "cdb.dec.ptr" : "cdb.dec.ptr.hi", method,
                          sa_cnt ? sa_cnt > (0x10000 << 2) ? "memset.long" : "memset.short" : "",
                          "cdb.exit", nullptr);
            }
        }
        addLoader("UPX1HEAD", "IDENTSTR", nullptr);
    }
}

#define OPTYPE(x) (((x) >> 13) & 0x7)
#define OPCODE(x) (((x) >> 10) & 0x7)
#define REG1(x)   (((x) >> 5) & 0x1f)
#define REG2(x)   ((x) &0x1f)

#define MIPS_IMM(a, b) ((((a) - (((b) &0x8000) >> 15)) << 16) | (b))

// Type
#define REGIMM 1
#define STORE  5
// Op
#define LUI    7
#define ADDIU  1
#define SW     3

#define IS_LUI(a)     ((OPTYPE(a) == REGIMM && OPCODE(a) == LUI))
#define IS_ADDIU(a)   ((OPTYPE(a) == REGIMM && OPCODE(a) == ADDIU))
#define IS_SW_ZERO(a) ((OPTYPE(a) == STORE && OPCODE(a) == SW) && REG2(a) == 0)

#define BSS_CHK_LIMIT (18)

bool PackPs1::findBssSection() {
    byte reg;
    const LE32 *const p1 = ACC_CCAST(const LE32 *, ibuf + (ih.epc - ih.tx_ptr));

    if ((ih.epc - ih.tx_ptr + (BSS_CHK_LIMIT * 4) + sizeof(bss_nfo)) > fdata_size)
        return false;

    // check 18 opcodes for sw zero,0(x)
    for (int i = BSS_CHK_LIMIT; i >= 0; i--) {
        upx_uint16_t op = p1[i] >> 16;
        if (IS_SW_ZERO(op)) {
            // found! get reg (x) for bss_start
            reg = REG1(op);
            for (; i >= 0; i--) {
                const bss_nfo *const p = ACC_CCAST(const bss_nfo *, &p1[i]);
                upx_uint16_t op1 = p->op1, op2 = p->op2;

                // check for la (x),bss_start
                if ((IS_LUI(op1) && REG2(op1) == reg) && (IS_ADDIU(op2) && REG1(op2) == reg)) {
                    op1 = p->op3, op2 = p->op4;

                    // check for la (y),bss_end
                    if (IS_LUI(op1) && IS_ADDIU(op2)) {
                        // bss section info found!
                        bss_start = MIPS_IMM(p->hi1, p->lo1);
                        bss_end = MIPS_IMM(p->hi2, p->lo2);

                        if (0 < ALIGN_DOWN(bss_end - bss_start, 4u)) {
                            unsigned wkmem_sz = M_IS_LZMA(ph.method) ? 32768 : 800;
                            unsigned end_offs = ih.tx_ptr + fdata_size + overlap;
                            if (bss_end > (end_offs + wkmem_sz))
                                return isCon || (!isCon && M_IS_LZMA(ph.method));
                            else
                                return false;
                        }
                    } else
                        return false;
                }
            }
        }
    }
    return false;
}

/*************************************************************************
//
**************************************************************************/

void PackPs1::pack(OutputFile *fo) {
    ibuf.alloc(fdata_size);
    obuf.allocForCompression(fdata_size);
    const byte *p_scan = ibuf + fdata_size;

    // read file
    fi->seek(PS_HDR_SIZE, SEEK_SET);
    fi->readx(ibuf, fdata_size);

    // scan EOF for 2048 bytes sector alignment
    // the removed space will secure in-place decompression
    while (!(*--p_scan)) {
        if (sa_cnt++ > (0x10000 << 5) || sa_cnt >= fdata_size - 1024)
            break;
    }

    if (sa_cnt > (0x10000 << 2))
        sa_cnt = ALIGN_DOWN(sa_cnt, 32u);
    else
        sa_cnt = ALIGN_DOWN(sa_cnt, 4u);

    // prepare packheader
    ph.u_len = (fdata_size - sa_cnt);
    ph.filter = 0;
    Filter ft(ph.level);

    // compress (max_match = 65535)
    upx_compress_config_t cconf;
    cconf.reset();
    cconf.conf_ucl.max_match = 65535;
    cconf.conf_lzma.max_num_probs = 1846 + (768 << 4); // ushort: ~28 KiB stack
    compressWithFilters(&ft, sa_cnt, &cconf);

    if (overlap) {
        opt->info_mode += !opt->info_mode ? 1 : 0;
        infoWarning("overlap - relocating load address (+%d bytes)", overlap);
        sa_cnt += overlap;
    }

    /*
        if (bss_start && bss_end && !foundBss)
            infoWarning("%s: .bss section too small - use stack", fi->getName());
    */

    unsigned lzma_init = 0;

    if (M_IS_LZMA(ph.method)) {
        sz_lunc = getLoaderSize();

        lzma_init = 0u - (sz_lunc - linker->getSymbolOffset("lzma.init"));
        defineDecompressorSymbols();
        linker->defineSymbol("entry", ih.epc);
        linker->defineSymbol("SC", sa_cnt > (0x10000 << 2) ? sa_cnt >> 5 : sa_cnt >> 2);
        relocateLoader();

        buildPart2 = true;
        buildLoader(&ft);
    }

    memcpy(&oh, &ih, sizeof(ih));

    unsigned sz_cbh;
    putBkupHeader((const byte *) &ih.epc, (byte *) &bh, &sz_cbh);

    if (ih.is_ptr < (EXE_BS | (PS_RAM_SIZE - PS_STACK_SIZE)))
        oh.is_ptr = (EXE_BS | (PS_RAM_SIZE - 16));

    if (ih.da_ptr != 0 || ih.da_len != 0 || ih.bs_ptr != 0 || ih.bs_len != 0)
        oh.da_ptr = oh.da_len = oh.bs_ptr = oh.bs_len = 0;

    const int lsize = getLoaderSize();

    unsigned filelen = ALIGN_UP(ih.tx_len, 4u);

    const unsigned decomp_data_start = ih.tx_ptr;
    const unsigned comp_data_start = (decomp_data_start + filelen + overlap) - ph.c_len;

    const int h_len = lsize - getLoaderSectionStart("UPX1HEAD");
    int d_len = 0;
    int e_len = 0;

    if (isCon) {
        e_len = lsize - h_len;
        d_len = e_len - getLoaderSectionStart("con.entry");
    } else {
        const char *entry_lzma = !foundBss ? "cdb.entry.lzma" : "bss.cdb.entry.lzma";

        d_len = (lsize - h_len) -
                getLoaderSectionStart(M_IS_LZMA(ph.method) ? entry_lzma : "cdb.entry");
        e_len = (lsize - d_len) - h_len;
    }

    linker->defineSymbol("entry", ih.epc);
    linker->defineSymbol("SC", MIPS_LO(sa_cnt > (0x10000 << 2) ? sa_cnt >> 5 : sa_cnt >> 2));
    linker->defineSymbol("DECO", decomp_data_start);
    linker->defineSymbol("ldr_sz", M_IS_LZMA(ph.method) ? sz_lunc + 16 : (d_len - pad_code));

    if (foundBss) {
        if (M_IS_LZMA(ph.method))
            linker->defineSymbol("wrkmem",
                                 bss_end - 160 - getDecompressorWrkmemSize() - (sz_lunc + 16));
        else
            linker->defineSymbol("wrkmem", bss_end - 16 - (d_len - pad_code));
    }

    const unsigned entry = comp_data_start - e_len;
    oh.epc = oh.tx_ptr = entry;
    oh.tx_len = ph.c_len + e_len;

    unsigned pad = 0;

    if (!opt->ps1_exe.no_align || !isCon) {
        pad = oh.tx_len;
        oh.tx_len = ALIGN_UP(oh.tx_len, CD_SEC + 0u);
        pad = oh.tx_len - pad;
        oh.tx_ptr -= pad;
    }

    ibuf.clear(0, fdata_size);
    byte *paddata = ibuf;

    if (M_IS_LZMA(ph.method)) {
        linker->defineSymbol("lzma_init_off", lzma_init);
        linker->defineSymbol("gb_e", linker->getSymbolOffset("gb8_e"));
    } else if (isCon)
        linker->defineSymbol("gb_e", linker->getSymbolOffset(is32Bit ? "gb32_e" : "gb8_e"));

    if (isCon) {
        linker->defineSymbol("PAD", pad_code);
        if (M_IS_LZMA(ph.method))
            linker->defineSymbol("DCRT", (entry + getLoaderSectionStart("lzma.exec")));
        else
            linker->defineSymbol("DCRT", (entry + (e_len - d_len)));
    } else {
        linker->defineSymbol("PSVR", FIX_PSVR);
        linker->defineSymbol("CPDO", comp_data_start);
        if (M_IS_LZMA(ph.method)) {
            unsigned entry_lzma =
                getLoaderSectionStart(!foundBss ? "cdb.entry.lzma" : "bss.cdb.entry.lzma");

            linker->defineSymbol("lzma_cpr", getLoaderSectionStart("lzma.exec") - entry_lzma);
        }
    }

    relocateLoader();
    // linker->dumpSymbols();
    MemBuffer loader(lsize);
    assert(lsize == getLoaderSize());
    memcpy(loader, getLoader(), lsize);
    patchPackHeader(loader, lsize);

    if (!isCon && M_IS_LZMA(ph.method) && (HD_CODE_OFS + d_len + h_len) > CD_SEC)
        throwInternalError("lzma --boot-only loader > 2048");

    // ps1_exe_t structure
    fo->write(&oh, sizeof(oh));
    fo->write(&bh, sz_cbh);
    // decompressor
    fo->write(loader + e_len, isCon ? h_len : (d_len + h_len));

    // header size is 2048 bytes + sector alignment
    fo->write(paddata, (pad + PS_HDR_SIZE) - fo->getBytesWritten());
    // entry
    fo->write(loader, e_len);
    // compressed body
    fo->write(obuf, ph.c_len);

    verifyOverlappingDecompression();
    if (!checkFinalCompressionRatio(fo))
        throwNotCompressible();

#if 0
    printf("%-13s: uncompressed  : %8d bytes\n", getName(), (int) ph.u_len);
    printf("%-13s: compressed    : %8d bytes\n", getName(), (int) ph.c_len);
    printf("%-13s: decompressor  : %8d bytes\n", getName(), (int) lsize - h_len - pad_code);
    printf("%-13s: header comp   : %8d bytes\n", getName(), (int) sz_cbh);
    printf("%-13s: overlap       : %8d bytes\n", getName(), (int) overlap);
    printf("%-13s: load address  : %08X bytes\n", getName(), (unsigned) oh.tx_ptr);
    printf("%-13s: code entry    : %08X bytes\n", getName(), (unsigned) oh.epc);
    printf("%-13s: bbs start     : %08X bytes\n", getName(), (unsigned) bss_start);
    printf("%-13s: bbs end       : %08X bytes\n", getName(), (unsigned) bss_end);
    printf("%-13s: eof in mem IF : %08X bytes\n", getName(), (unsigned) ih.tx_ptr + ih.tx_len);
    printf("%-13s: eof in mem OF : %08X bytes\n", getName(), (unsigned) oh.tx_ptr + oh.tx_len);
    byte i = 0;
    if (isCon) {
        if (foundBss)
            i = 1;
    } else {
        i = 2;
        if (M_IS_LZMA(ph.method)) {
            if (!foundBss)
                i = 3;
            else
                i = 4;
        }
    }
    const char *const loader_method[] = {"con/stack", "con/bss", "cdb", "cdb/stack", "cdb/bss"};
    char method_name[32 + 1];
    set_method_name(method_name, sizeof(method_name), ph.method, ph.level);
    printf("%-13s: methods       : %s, %s\n", getName(), method_name, loader_method[i]);
#endif
}

/*************************************************************************
//
**************************************************************************/

tribool PackPs1::canUnpack() {
    if (!readFileHeader())
        return false;
    if (!readPackHeader(CD_SEC))
        return false;
    // check header as set by packer
    if (!readBkupHeader() || ph.c_len >= fdata_size)
        throwCantUnpack("header damaged");
    // generic check
    if (!checkFileHeader())
        throwCantUnpack("unsupported header flags");
    return true;
}

/*************************************************************************
//
**************************************************************************/

void PackPs1::unpack(OutputFile *fo) {
    // restore orig exec hdr
    memcpy(&oh, &ih, sizeof(ih));
    memcpy((void *) &oh.epc, &bh, SZ_IH_BKUP);

    // check for removed sector alignment
    assert(oh.tx_len >= ph.u_len);
    const unsigned pad = oh.tx_len - ph.u_len;

    ibuf.alloc(fdata_size > PS_HDR_SIZE ? fdata_size : PS_HDR_SIZE);
    obuf.allocForDecompression(ph.u_len, pad);

    fi->seek(PS_HDR_SIZE, SEEK_SET);
    fi->readx(ibuf, fdata_size);

    // decompress
    decompress(ibuf + (fdata_size - ph.c_len), obuf);

    // write decompressed file
    if (fo) {
        // write header
        fo->write(&oh, sizeof(oh));
        // align the ps exe header (mode 2 sector data size)
        ibuf.clear();
        fo->write(ibuf, PS_HDR_SIZE - fo->getBytesWritten());
        // write uncompressed data + pad
        obuf.clear(ph.u_len, pad);
        fo->write(obuf, ph.u_len + pad);
    }
}

/* vim:set ts=4 sw=4 et: */
