/*****************************************************************************/
/* SFileVerify.cpp                        Copyright (c) Ladislav Zezula 2010 */
/*---------------------------------------------------------------------------*/
/* Support for conversion of UTF-8 <-> File name                             */
/*                                                                           */
/* File names in the MPQs are assumed to be UTF-8. However, bad sequences    */
/* or filename unsafe characters are allowed in the list files, but won't    */
/* work in unpacking files from MPQ to a local file.                         */
/*                                                                           */
/* This module contains cross-platform comparable conversion between UTF-8   */
/* and file names that will produce identical file names across platforms.   */
/*---------------------------------------------------------------------------*/
/*   Date    Ver   Who  Comment                                              */
/* --------  ----  ---  -------                                              */
/* 31.10.24  1.00  Lad  Created                                              */
/*****************************************************************************/

#define __STORMLIB_SELF__
#include "StormLib.h"
#include "StormCommon.h"

//-----------------------------------------------------------------------------
// Local defines

#define MAX_INVALID_CHARS               128         // Maximum number of invalid characters in a row

//-----------------------------------------------------------------------------
// Conversion tables

const unsigned char SMemCharToByte[0x80] =
{
    //   00    01    02    03    04    05    06    07    08    09    0A    0B    0C    0D    0E    0F
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x10
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x20
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x30
        0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x40
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x50
        0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x60
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF  // 0x70
};

//-----------------------------------------------------------------------------
// Local functions

// Bit mask of characters that are file name safe. We will maintain
// the same charset even on non-Windows in order to keep the file names equal
static unsigned int FileNameSafeChars[4] =
{
    0x00000000, 0x2BFF7BFB, 0xFFFFFFFF, 0xEFFFFFFF      // Windows: [0x20-0x7F], except 0x22, 0x2A, 0x2F, 0x3A, 0x3C, 0x3E, 0x3F, 0x7C
//  0xfffffffe, 0xffff7fff, 0xffffffff, 0xffffffff      // Linux:   [0x01-0x7F], except 0x2F
};

static bool UTF8_IsBadFileNameCharacter(DWORD ch)
{
    // It is guaranteed that the character is in range of 0x00 - 0x7F
    assert(ch < 0x80);

    // Use the bit from the table
    return (FileNameSafeChars[ch / 32] & (1 << (ch % 32))) ? false : true;
}

static DWORD UTF8_DecodeSequence(const BYTE * pbString, BYTE BitsMask, size_t ccFollowBytes, DWORD dwMinValue, DWORD dwMaxValue, DWORD & dwCodePoint, size_t & ccBytesEaten)
{
    const BYTE * pbSaveString = pbString;
    DWORD dwAccumulator;

    // Extract the low bits from the leading byte
    dwAccumulator = pbString[0] & BitsMask;
    ccBytesEaten = 1;
    pbString++;

    // Process the follow-up bytes
    for(size_t i = 0; i < ccFollowBytes; i++)
    {
        // Every follow-up byte in the UTF-8 sequence must start with 10xxxxxx
        if((pbString[0] & 0xC0) != 0x80)
            return ERROR_NO_UNICODE_TRANSLATION;

        // Add 6 bits to the accumulator
        dwAccumulator = (dwAccumulator << 6) | (*pbString++ & 0x3F);
    }

    // Check whether the code point is in the given range
    if(!(dwMinValue <= dwAccumulator && dwAccumulator <= dwMaxValue))
        return ERROR_INVALID_DATA;

    // Give the number of bytes eaten and the decoded code point
    ccBytesEaten = (pbString - pbSaveString);
    dwCodePoint = dwAccumulator;
    return ERROR_SUCCESS;
}

// https://en.wikipedia.org/wiki/UTF-8
static DWORD UTF8_DecodeCodePoint(const BYTE * pbString, const BYTE * pbStringEnd, DWORD & dwCodePoint, size_t & ccBytesEaten)
{
    // Reset the number of bytes eaten
    dwCodePoint = SFILE_UTF8_INVALID_CHARACTER;
    ccBytesEaten = 0;

    if(pbString < pbStringEnd)
    {
        // At least one byte will be eaten
        ccBytesEaten = 1;

        // 1st code point (0x00 - 0x7F, 1 byte)
        if(pbString[0] <= 0x7F)
        {
            // This is the perfect spot to check for filename-unsafe characters
            if(UTF8_IsBadFileNameCharacter(pbString[0]))
                return ERROR_NO_UNICODE_TRANSLATION;

            // Decode the 1-byte sequence
            dwCodePoint = pbString[0];
            return ERROR_SUCCESS;
        }

        // 2nd code point (0x80 - 0x7FF, 2 bytes)
        if((pbString[0] & 0xE0) == 0xC0 && (pbString + 2) <= pbStringEnd)
        {
            // Decode the 2-byte sequence
            return UTF8_DecodeSequence(pbString, 0x1F, 1, 0x80, 0x7FF, dwCodePoint, ccBytesEaten);
        }

        // 3rd code point (0x800 - 0xFFFF, 3 bytes)
        // Note: MultiByteToWideChar will not decode 0xE0 0xBF 0xBF (--> 0x0FFF),
        if((pbString[0] & 0xF0) == 0xE0 && (pbString + 3) <= pbStringEnd)
        {
            // Decode the 3-byte sequence
            return UTF8_DecodeSequence(pbString, 0x0F, 2, 0x800, 0xFFFF, dwCodePoint, ccBytesEaten);
        }

        // 4th code point (0x10000 - 0x10FFFF, 4 bytes)
        if((pbString[0] & 0xF8) == 0xF0 && (pbString + 4) <= pbStringEnd)
        {
            // Try to decode 4-byte sequence
            return UTF8_DecodeSequence(pbString, 0x07, 3, 0x10000, SFILE_UNICODE_MAX, dwCodePoint, ccBytesEaten);
        }

        // An invalid UTF-8 sequence encountered
        return ERROR_NO_UNICODE_TRANSLATION;
    }

    // No bytes available. Should never happen
    assert(false);
    return ERROR_BUFFER_OVERFLOW;
}

static size_t UTF8_EncodeSequence(DWORD dwCodePoint, BYTE LeadingByte, DWORD dwFollowByteCount, LPBYTE Utf8Buffer)
{
    DWORD dwByteShift = dwFollowByteCount * 6;

    // Encode the highest byte
    Utf8Buffer[0] = (BYTE)(LeadingByte | (dwCodePoint >> dwByteShift));
    dwByteShift -= 6;

    // Encode the follow bytes
    for(DWORD i = 0; i < dwFollowByteCount; i++)
    {
        // The follow byte must be 10xxxxxx
        Utf8Buffer[i + 1] = (BYTE)(0x80 | ((dwCodePoint >> dwByteShift) & 0x3F));
        dwByteShift -= 6;
    }

    return dwFollowByteCount + 1;
}

static size_t UTF8_EncodeCodePoint(DWORD dwCodePoint, LPBYTE Utf8Buffer)
{
    // 0x00 - 0x7F, 1 byte
    if(dwCodePoint < 0x80)
        return UTF8_EncodeSequence(dwCodePoint, 0x00, 0, Utf8Buffer);

    // 0x80 - 0x7FF
    if(dwCodePoint < 0x800)
        return UTF8_EncodeSequence(dwCodePoint, 0xC0, 1, Utf8Buffer);

    // 0x800 - 0xFFFF
    if(dwCodePoint < 0x10000)
        return UTF8_EncodeSequence(dwCodePoint, 0xE0, 2, Utf8Buffer);

    // 0x800 - 0xFFFF
    if(dwCodePoint < 0x110000)
        return UTF8_EncodeSequence(dwCodePoint, 0xF0, 3, Utf8Buffer);

    // Should never happen
    assert(false);
    return 0;
}

static size_t UTF8_FlushInvalidChars(LPTSTR szBuffer, size_t ccBuffer, size_t nOutLength, LPBYTE InvalidChars, size_t nInvalidChars)
{
    // Case 0: No invalid char -> do nothing
    if(nInvalidChars == 0)
    {
        return nOutLength;
    }

    // Case 1: One invalid char -> %xx (compatible with previous versions of MPQ Editor)
    if(nInvalidChars == 1)
    {
        // Space for 3 characters needed
        if(szBuffer != NULL && (nOutLength + 3) <= ccBuffer)
        {
            szBuffer[nOutLength] = '%';
            SMemBinToStr(szBuffer + nOutLength + 1, ccBuffer - 1, InvalidChars, 1);
        }
        return nOutLength + 3;
    }

    // Case 1: More than one invalid char -> %u[xxyyzz]
    else
    {
        // Enough space for %u[xxyyzz]
        size_t nLengthNeeded = nInvalidChars * 2 + 4;

        // Space for 4 characters needed
        if(szBuffer != NULL && (nOutLength + nLengthNeeded) <= ccBuffer)
        {
            memcpy(szBuffer + nOutLength, _T("%u["), 6);

            SMemBinToStr(szBuffer + nOutLength + 3, ccBuffer - 3, InvalidChars, nInvalidChars);

            szBuffer[nOutLength + nLengthNeeded - 1] = ']';
            szBuffer[nOutLength + nLengthNeeded] = 0;
        }
        return nOutLength + nLengthNeeded;
    }
}

size_t UTF8_FlushBinBuffer(LPBYTE pbBuffer, size_t ccBuffer, size_t nOutLength, LPBYTE BinBuffer, size_t nByteCount)
{
    if(pbBuffer != NULL && (nOutLength + nByteCount) < ccBuffer)
        memcpy(pbBuffer + nOutLength, BinBuffer, nByteCount);
    return nOutLength + nByteCount;
}

#ifdef STORMLIB_WIDE_CHAR
static size_t UTF16_EncodeCodePoint(DWORD dwCodePoint, unsigned short * Utf16Buffer)
{
    // https://en.wikipedia.org/wiki/UTF-16
    if(dwCodePoint <= 0xFFFF)
    {
        Utf16Buffer[0] = (unsigned short)(dwCodePoint);
        return 1;
    }

    if(dwCodePoint <= SFILE_UNICODE_MAX)
    {
        // Fix the code point
        dwCodePoint -= 0x10000;

        // Split the code point to two 10-bit values
        Utf16Buffer[0] = (unsigned short)(0xD800 + (dwCodePoint >> 10));    // High 6 bytes
        Utf16Buffer[1] = (unsigned short)(0xDC00 + (dwCodePoint & 0x3FF));  // Low 10 bytes
        return 2;
    }

    // Should never happen
    assert(false);
    return 0;
}

static DWORD UTF16_DecodeCodePoint(LPCTSTR szString, LPCTSTR szStringEnd, DWORD & dwCodePoint, size_t & ccCharsEaten)
{
    // Reset the number of bytes eaten
    dwCodePoint = SFILE_UTF8_INVALID_CHARACTER;
    ccCharsEaten = 0;

    if(szString < szStringEnd)
    {
        // At least one char will be eaten
        ccCharsEaten = 1;

        // Check for an invalid surrogate pair
        if(0xDC00 <= szString[0] && szString[0] <= 0xDFFF)
        {
            dwCodePoint = SFILE_UTF8_INVALID_CHARACTER;
            return ERROR_NO_UNICODE_TRANSLATION;
        }

        // Check for a valid surrogate pair
        if(0xD800 <= szString[0] && szString[0] <= 0xDBFF && (szString + 1) < szStringEnd)
        {
            dwCodePoint = ((szString[0] - 0xD800) << 10) | (szString[1] - 0xDC00) + 0x10000;
            ccCharsEaten = 2;
            return ERROR_SUCCESS;
        }

        // Direct encoding
        dwCodePoint = szString[0];
        ccCharsEaten = 1;
        return ERROR_SUCCESS;
    }

    // No bytes available. Should never happen
    assert(false);
    return ERROR_BUFFER_OVERFLOW;
}
#endif

size_t UTF16_IsEncodedCharSequence(LPCTSTR szString, LPCTSTR szStringEnd, LPBYTE BinBuffer)
{
    size_t nEncodedChars = 0;

    if((szString + 1) < szStringEnd && *szString++ == '%')
    {
        if((szString + 1) < szStringEnd && *szString++ == 'u')
        {
            if((szString + 1) < szStringEnd && *szString++ == '[')
            {
                // Keep going as long as we can convert
                for(size_t i = 0; i < MAX_INVALID_CHARS; i++)
                {
                    if(szString + (i * 2) >= szStringEnd)
                        break;
                    if(szString[i * 2] == ']')
                        break;
                    nEncodedChars++;
                }

                // Did we encounter the end of the string?
                if(szString + (nEncodedChars * 2) + 1 <= szStringEnd && szString[nEncodedChars * 2] == ']')
                {
                    TCHAR HexaString[MAX_INVALID_CHARS * 2 + 1];

                    // Copy the hexadecimal string
                    memcpy(HexaString, szString, (nEncodedChars * 2) * sizeof(TCHAR));
                    HexaString[nEncodedChars * 2] = 0;

                    // Try to decode the hexa string
                    if(SMemStrToBin(HexaString, BinBuffer, nEncodedChars) == ERROR_SUCCESS)
                    {
                        return nEncodedChars;
                    }
                }
            }
        }
    }
    return 0;
}

//-----------------------------------------------------------------------------
// Public (exported) functions

// Conversion of MPQ file name to file-name-safe string
DWORD WINAPI SMemUTF8ToFileName(
    LPTSTR szBuffer,                // Pointer to the output buffer. If NULL, the function will calulate the needed length
    size_t ccBuffer,                // Length of the output buffer (must include EOS)
    const void * lpString,          // Pointer to the begin of the string
    const void * lpStringEnd,       // Pointer to the end of string. If NULL, it's assumed to be zero-terminated
    DWORD dwFlags,                  // Additional flags
    size_t * pOutLength = NULL)     // Pointer to a variable that receives the needed length (optional)
{
    const BYTE * pbStringEnd = (const BYTE *)lpStringEnd;
    const BYTE * pbString = (const BYTE *)lpString;
    DWORD dwErrCode = ERROR_SUCCESS;
    size_t nInvalidChars = 0;
    size_t nOutLength = 0;
    BYTE InvalidChars[MAX_INVALID_CHARS];

    // Set the end of the input if not specified
    if(pbStringEnd == NULL)
        pbStringEnd = pbString + strlen((char *)pbString);

    // Keep conversion as long
    while(pbString < pbStringEnd)
    {
        size_t ccBytesEaten = 0;
        size_t nCharLength;
        DWORD dwCodePoint = 0;

        // Decode the single UTF-8 char
        if((dwErrCode = UTF8_DecodeCodePoint(pbString, pbStringEnd, dwCodePoint, ccBytesEaten)) != ERROR_SUCCESS)
        {
            // Exactly one byte should be eaten on error
            assert(ccBytesEaten == 1);

            // If invalid chars are allowed, we replace the result with 0xFFFD
            if(dwFlags & SFILE_UTF8_ALLOW_INVALID_CHARS)
            {
                // Replace the code point with invalid marker and continue on the next character
                dwCodePoint = SFILE_UTF8_INVALID_CHARACTER;
                dwErrCode = ERROR_SUCCESS;
            }

            // If the invalid chars are not allowed, we put the invalid char to the stack
            else
            {
                // Flush the invalid characters, if full
                if(nInvalidChars >= _countof(InvalidChars))
                {
                    nOutLength = UTF8_FlushInvalidChars(szBuffer, ccBuffer, nOutLength, InvalidChars, nInvalidChars);
                    nInvalidChars = 0;
                }

                // Put the invalid char to the stack
                InvalidChars[nInvalidChars++] = pbString[0];
                pbString++;
                continue;
            }
        }

        // Check whether the unicode char is not out of range
        assert(dwCodePoint <= SFILE_UNICODE_MAX);

        // Move the source pointer by the number of bytes eaten
        pbString = pbString + ccBytesEaten;

        // Flush the invalid characters, if any
        nOutLength = UTF8_FlushInvalidChars(szBuffer, ccBuffer, nOutLength, InvalidChars, nInvalidChars);
        nInvalidChars = 0;

#ifdef STORMLIB_WIDE_CHAR
        {
            unsigned short Utf16Buffer[2];

            // Encode the code point into UTF-16
            nCharLength = UTF16_EncodeCodePoint(dwCodePoint, Utf16Buffer);

            // Write the encoded UTF-16 to the output buffer, if present
            if(szBuffer != NULL && (nOutLength + nCharLength) < ccBuffer)
            {
                memcpy(szBuffer + nOutLength, Utf16Buffer, nCharLength * sizeof(unsigned short));
            }
        }
#else
        {
            BYTE Utf8Buffer[4];

            // Encode the code point into UTF-8
            nCharLength = UTF8_EncodeCodePoint(dwCodePoint, Utf8Buffer);

            // Write the encoded UTF-16 to the output buffer, if present
            if(szBuffer != NULL && (nOutLength + nCharLength) < ccBuffer)
            {
                memcpy(szBuffer + nOutLength, Utf8Buffer, nCharLength);
            }
        }
#endif

        // Increment the output length
        nOutLength = nOutLength + nCharLength;
    }

    // Flush the invalid characters, if any
    nOutLength = UTF8_FlushInvalidChars(szBuffer, ccBuffer, nOutLength, InvalidChars, nInvalidChars);
    nInvalidChars = 0;

    // Terminate the string with zero, if we still have space
    if(szBuffer != NULL && nOutLength < ccBuffer)
        szBuffer[nOutLength] = 0;
    nOutLength++;

    // Give the output length, if required
    if(pOutLength != NULL)
        pOutLength[0] = nOutLength;
    return dwErrCode;
}

DWORD WINAPI SMemFileNameToUTF8(
    void * lpBuffer,                // Pointer to the output buffer. If NULL, the function will calulate the needed length
    size_t ccBuffer,                // Length of the output buffer (must include EOS)
    const TCHAR * szString,         // Pointer to the begin of the string
    const TCHAR * szStringEnd,      // Pointer to the end of string. If NULL, it's assumed to be zero-terminated
    DWORD /* dwFlags */,            // Additional flags
    size_t * pOutLength = NULL)     // Pointer to a variable that receives the needed length in bytes (optional)
{
    LPBYTE pbBuffer = (LPBYTE)lpBuffer;
    size_t nOutLength = 0;
    DWORD dwErrCode = ERROR_SUCCESS;

    // Set the end of the input if not specified
    if(szStringEnd == NULL)
        szStringEnd = szString + _tcslen(szString);

    // Keep conversion as long
    while(szString < szStringEnd)
    {
        size_t ccCharsEaten = 0;
        size_t nUtf8Length;
        DWORD dwCodePoint = 0;
        BYTE Utf8Buffer[MAX_INVALID_CHARS];

        // Check for encoded sequence of bytes
        if(szString[0] == '%')
        {
            // If there is a single hexa number ("%c7"), decode that number
            if((szString + 3) <= szStringEnd)
            {
                TCHAR HexaString[3] = {0};

                HexaString[0] = szString[1];
                HexaString[1] = szString[2];
                if(SMemStrToBin(HexaString, Utf8Buffer, 1) == ERROR_SUCCESS)
                {
                    nOutLength = UTF8_FlushBinBuffer(pbBuffer, ccBuffer, nOutLength, Utf8Buffer, 1);
                    szString += 3;
                    continue;
                }
            }

            // If there is an escaped sequence ("%u[aabbcc]"), decode that sequence
            if((nUtf8Length = UTF16_IsEncodedCharSequence(szString, szStringEnd, Utf8Buffer)) != 0)
            {
                nOutLength = UTF8_FlushBinBuffer(pbBuffer, ccBuffer, nOutLength, Utf8Buffer, nUtf8Length);
                szString += (nUtf8Length * 2) + 4;
                continue;
            }
        }

#ifdef STORMLIB_WIDE_CHAR
        // Try to decode the code point from UTF-16
        if((dwErrCode = UTF16_DecodeCodePoint(szString, szStringEnd, dwCodePoint, ccCharsEaten)) != ERROR_SUCCESS)
            return dwErrCode;
#else
        // Try to decode the code point from UTF-16
        if((dwErrCode = UTF8_DecodeCodePoint((const BYTE *)szString, (const BYTE *)szStringEnd, dwCodePoint, ccCharsEaten)) != ERROR_SUCCESS)
            return dwErrCode;
#endif

        // Check whether the unicode char is not out of range
        assert(dwCodePoint <= SFILE_UNICODE_MAX);

        // Move the source pointer by the number of bytes eaten
        szString = szString + ccCharsEaten;

        // Encode the UNICODE char
        nUtf8Length = UTF8_EncodeCodePoint(dwCodePoint, Utf8Buffer);

        // Do we have enough space in the buffer?
        if(pbBuffer != NULL && (nOutLength + nUtf8Length) < ccBuffer)
        {
            // Write the encoded UTF-16 to the output
            memcpy(pbBuffer + nOutLength, Utf8Buffer, nUtf8Length);
        }

        // Increment the output length
        nOutLength = nOutLength + nUtf8Length;
    }

    // Terminate the string with zero, if we still have space
    if(pbBuffer != NULL && nOutLength < ccBuffer)
        pbBuffer[nOutLength] = 0;
    nOutLength++;

    // Give the output length, if required
    if(pOutLength != NULL)
        pOutLength[0] = nOutLength;
    return dwErrCode;
}
