/* compress_ucl.cpp --

   This file is part of the UPX executable compressor.

   Copyright (C) 1996-2025 Markus Franz Xaver Johannes Oberhumer
   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
   <markus@oberhumer.com>
 */

#include "../conf.h"
#include "compress.h"

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

static int convert_errno_from_ucl(int r) {
    switch (r) {
    case UCL_E_OK:
        return UPX_E_OK;
    case UCL_E_ERROR:
        return UPX_E_ERROR;
    case UCL_E_OUT_OF_MEMORY:
        return UPX_E_OUT_OF_MEMORY;
    case UCL_E_NOT_COMPRESSIBLE:
        return UPX_E_NOT_COMPRESSIBLE;
    case UCL_E_INPUT_OVERRUN:
        return UPX_E_INPUT_OVERRUN;
    case UCL_E_OUTPUT_OVERRUN:
        return UPX_E_OUTPUT_OVERRUN;
    case UCL_E_LOOKBEHIND_OVERRUN:
        return UPX_E_LOOKBEHIND_OVERRUN;
    case UCL_E_EOF_NOT_FOUND:
        return UPX_E_EOF_NOT_FOUND;
    case UCL_E_INPUT_NOT_CONSUMED:
        return UPX_E_INPUT_NOT_CONSUMED;
    case UCL_E_INVALID_ARGUMENT:
        return UPX_E_INVALID_ARGUMENT;
    // UCL extra:
    case UCL_E_OVERLAP_OVERRUN:
        return UPX_E_ERROR;
    default:
        break;
    }
    return UPX_E_ERROR;
}

extern "C" {
static void __UCL_CDECL wrap_nprogress_ucl(ucl_uint a, ucl_uint b, int state, ucl_voidp user) {
    if (state != -1 && state != 3)
        return;
    upx_callback_t *cb = (upx_callback_t *) user;
    if (cb && cb->nprogress)
        cb->nprogress(cb, a, b);
}
}

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

int upx_ucl_compress(const upx_bytep src, unsigned src_len, upx_bytep dst, unsigned *dst_len,
                     upx_callback_t *cb_parm, int method, int level,
                     const upx_compress_config_t *cconf_parm, upx_compress_result_t *cresult) {
    int r;
    assert(level > 0);
    assert(cresult != nullptr);

    COMPILE_TIME_ASSERT(sizeof(ucl_compress_config_t) == sizeof(REAL_ucl_compress_config_t))

    ucl_progress_callback_t cb;
    cb.callback = nullptr;
    cb.user = nullptr;
    if (cb_parm && cb_parm->nprogress) {
        cb.callback = wrap_nprogress_ucl;
        cb.user = cb_parm;
    }

    ucl_compress_config_t cconf;
    cconf.reset();
    if (cconf_parm) {
        // cconf = cconf_parm->conf_ucl; // struct copy
        memcpy(&cconf, &cconf_parm->conf_ucl, sizeof(cconf));
    }

    ucl_uint *const res = cresult->result_ucl.result;
    // assume no info available - fill in worst case results
    // res[0] = 1;        // min_offset_found - NOT USED
    res[1] = src_len - 1; // max_offset_found
    // res[2] = 2;        // min_match_found - NOT USED
    res[3] = src_len - 1; // max_match_found
    // res[4] = 1;        // min_run_found - NOT USED
    res[5] = src_len; // max_run_found
    res[6] = 1;       // first_offset_found
    // res[7] = 999999;   // same_match_offsets_found - NOT USED

    // prepare bit-buffer settings
    cconf.bb_endian = 0;
    cconf.bb_size = 0;
    if (method >= M_NRV2B_LE32 && method <= M_NRV2E_LE16) {
        static const upx_uint8_t sizes[3] = {32, 8, 16};
        cconf.bb_size = sizes[(method - M_NRV2B_LE32) % 3];
    } else {
        throwInternalError("unknown compression method");
        return UPX_E_ERROR;
    }

    // optimize compression params
    if (level <= 3 && cconf.max_offset == UCL_UINT_MAX)
        cconf.max_offset = 8 * 1024 - 1;
    else if (level == 4 && cconf.max_offset == UCL_UINT_MAX)
        cconf.max_offset = 32 * 1024 - 1;

    if M_IS_NRV2B (method)
        r = ucl_nrv2b_99_compress(src, src_len, dst, dst_len, &cb, level, &cconf, res);
    else if M_IS_NRV2D (method)
        r = ucl_nrv2d_99_compress(src, src_len, dst, dst_len, &cb, level, &cconf, res);
    else if M_IS_NRV2E (method)
        r = ucl_nrv2e_99_compress(src, src_len, dst, dst_len, &cb, level, &cconf, res);
    else {
        throwInternalError("unknown compression method");
        return UPX_E_ERROR;
    }

    // make sure first_offset_found is set
    if (res[6] == 0)
        res[6] = 1;

    return convert_errno_from_ucl(r);
}

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

int upx_ucl_decompress(const upx_bytep src, unsigned src_len, upx_bytep dst, unsigned *dst_len,
                       int method, const upx_compress_result_t *cresult) {
    int r;

    switch (method) {
    case M_NRV2B_8:
        r = ucl_nrv2b_decompress_safe_8(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2B_LE16:
        r = ucl_nrv2b_decompress_safe_le16(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2B_LE32:
        r = ucl_nrv2b_decompress_safe_le32(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2D_8:
        r = ucl_nrv2d_decompress_safe_8(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2D_LE16:
        r = ucl_nrv2d_decompress_safe_le16(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2D_LE32:
        r = ucl_nrv2d_decompress_safe_le32(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2E_8:
        r = ucl_nrv2e_decompress_safe_8(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2E_LE16:
        r = ucl_nrv2e_decompress_safe_le16(src, src_len, dst, dst_len, nullptr);
        break;
    case M_NRV2E_LE32:
        r = ucl_nrv2e_decompress_safe_le32(src, src_len, dst, dst_len, nullptr);
        break;
    default:
        throwInternalError("unknown decompression method");
        return UPX_E_ERROR;
    }

    UNUSED(cresult);
    return convert_errno_from_ucl(r);
}

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

int upx_ucl_test_overlap(const upx_bytep buf, const upx_bytep tbuf, unsigned src_off,
                         unsigned src_len, unsigned *dst_len, int method,
                         const upx_compress_result_t *cresult) {
    int r;
    UNUSED(tbuf); // not needed for UCL

    switch (method) {
    case M_NRV2B_8:
        r = ucl_nrv2b_test_overlap_8(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2B_LE16:
        r = ucl_nrv2b_test_overlap_le16(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2B_LE32:
        r = ucl_nrv2b_test_overlap_le32(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2D_8:
        r = ucl_nrv2d_test_overlap_8(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2D_LE16:
        r = ucl_nrv2d_test_overlap_le16(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2D_LE32:
        r = ucl_nrv2d_test_overlap_le32(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2E_8:
        r = ucl_nrv2e_test_overlap_8(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2E_LE16:
        r = ucl_nrv2e_test_overlap_le16(buf, src_off, src_len, dst_len, nullptr);
        break;
    case M_NRV2E_LE32:
        r = ucl_nrv2e_test_overlap_le32(buf, src_off, src_len, dst_len, nullptr);
        break;
    default:
        throwInternalError("unknown decompression method");
        return UPX_E_ERROR;
    }

    UNUSED(cresult);
    return convert_errno_from_ucl(r);
}

/*************************************************************************
// misc
**************************************************************************/

extern "C" {
static ucl_voidp __UCL_CDECL my_malloc(ucl_uint n) { return upx_calloc(n, 1); }
static void __UCL_CDECL my_free(ucl_voidp p) { free(p); }
} // extern "C"

int upx_ucl_init(void) {
#if (ACC_CC_MSC && ACC_ARCH_I386) && (_MSC_VER >= 1940)
    (void) ucl_init(); // TODO later
#else
    if (ucl_init() != UCL_E_OK)
        return -1;
#endif
    if (UCL_VERSION != ucl_version() || strcmp(UCL_VERSION_STRING, ucl_version_string()) != 0)
        return -2;
    ucl_set_malloc_hooks(my_malloc, my_free);
    return 0;
}

const char *upx_ucl_version_string(void) { return ucl_version_string(); }

unsigned upx_ucl_adler32(const void *buf, unsigned len, unsigned adler) {
    return ucl_adler32(adler, (const ucl_bytep) buf, len);
}

#if 0 // UNUSED
unsigned upx_ucl_crc32(const void *buf, unsigned len, unsigned crc) {
    return ucl_crc32(crc, (const ucl_bytep) buf, len);
}
#endif

/*************************************************************************
// doctest checks
**************************************************************************/

#if DEBUG && !defined(DOCTEST_CONFIG_DISABLE) && 1

#include "../util/membuffer.h"

static bool check_ucl(const int method, const unsigned expected_c_len) {
    const unsigned u_len = 16384;
    const unsigned c_extra = 4096;
    MemBuffer u_buf, c_buf, d_buf;
    unsigned c_len, d_len;
    upx_compress_result_t cresult;
    int r;
    const int level = 3; // don't waste time

    u_buf.alloc(u_len);
    memset(u_buf, 0, u_len);
    c_buf.allocForCompression(u_len, c_extra);
    d_buf.allocForDecompression(u_len);

    c_len = c_buf.getSize() - c_extra;
    r = upx_ucl_compress(raw_bytes(u_buf, u_len), u_len, raw_index_bytes(c_buf, c_extra, c_len),
                         &c_len, nullptr, method, level, NULL_cconf, &cresult);
    if (r != 0 || c_len != expected_c_len)
        return false;

    d_len = d_buf.getSize();
    r = upx_ucl_decompress(raw_index_bytes(c_buf, c_extra, c_len), c_len, raw_bytes(d_buf, d_len),
                           &d_len, method, nullptr);
    if (r != 0 || d_len != u_len || memcmp(u_buf, d_buf, u_len) != 0)
        return false;

    d_len = u_len - 1;
    r = upx_ucl_decompress(raw_index_bytes(c_buf, c_extra, c_len), c_len, raw_bytes(d_buf, d_len),
                           &d_len, method, nullptr);
    if (r == 0)
        return false;

    // TODO: rewrite Packer::findOverlapOverhead() so that we can test it here
    // unsigned x_len = d_len;
    // r = upx_ucl_test_overlap(c_buf, u_buf, c_extra, c_len, &x_len, method, nullptr);
    return true;
}

TEST_CASE("compress_ucl") {
    CHECK(check_ucl(M_NRV2B_8, 34));
    CHECK(check_ucl(M_NRV2B_LE16, 34));
    CHECK(check_ucl(M_NRV2B_LE32, 34));
    CHECK(check_ucl(M_NRV2D_8, 32));
    CHECK(check_ucl(M_NRV2D_LE16, 32));
    CHECK(check_ucl(M_NRV2D_LE32, 34));
    CHECK(check_ucl(M_NRV2E_8, 32));
    CHECK(check_ucl(M_NRV2E_LE16, 32));
    CHECK(check_ucl(M_NRV2E_LE32, 34));
}

#endif // DEBUG

TEST_CASE("upx_ucl_decompress") {
    const byte *c_data;
    byte d_buf[16];
    unsigned d_len;
    int r;

    c_data = (const byte *) "\x92\xff\x10\x00\x00\x00\x00\x00\x48\xff";
    d_len = 16;
    r = upx_ucl_decompress(c_data, 10, d_buf, &d_len, M_NRV2B_8, nullptr);
    CHECK((r == 0 && d_len == 16));
    r = upx_ucl_decompress(c_data, 9, d_buf, &d_len, M_NRV2B_8, nullptr);
    CHECK(r == UPX_E_INPUT_OVERRUN);
    d_len = 15;
    r = upx_ucl_decompress(c_data, 10, d_buf, &d_len, M_NRV2B_8, nullptr);
    CHECK(r == UPX_E_OUTPUT_OVERRUN);

    c_data = (const byte *) "\x92\xff\x10\x92\x49\x24\x92\xa0\xff";
    d_len = 16;
    r = upx_ucl_decompress(c_data, 9, d_buf, &d_len, M_NRV2D_8, nullptr);
    CHECK((r == 0 && d_len == 16));
    r = upx_ucl_decompress(c_data, 8, d_buf, &d_len, M_NRV2D_8, nullptr);
    CHECK(r == UPX_E_INPUT_OVERRUN);
    d_len = 15;
    r = upx_ucl_decompress(c_data, 9, d_buf, &d_len, M_NRV2D_8, nullptr);
    CHECK(r == UPX_E_OUTPUT_OVERRUN);

    c_data = (const byte *) "\x90\xff\xb0\x92\x49\x24\x92\xa0\xff";
    d_len = 16;
    r = upx_ucl_decompress(c_data, 9, d_buf, &d_len, M_NRV2E_8, nullptr);
    CHECK((r == 0 && d_len == 16));
    r = upx_ucl_decompress(c_data, 8, d_buf, &d_len, M_NRV2E_8, nullptr);
    CHECK(r == UPX_E_INPUT_OVERRUN);
    d_len = 15;
    r = upx_ucl_decompress(c_data, 9, d_buf, &d_len, M_NRV2E_8, nullptr);
    CHECK(r == UPX_E_OUTPUT_OVERRUN);
    UNUSED(r);
}

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