1. 발단



모든 일에는 시작이 있는 법…

발단은 메모장2 mod 포스팅에 달린 댓글 하나였다.


애초에 이 기능을 제대로 써볼 생각도 없었던지라 생각도 못했는데, 소스를 읽다보니 뭔가 많이 이상하다.

이 기능은 기본적으로 유니코드UTF-8UrlEscape 순으로 변환하는 게 일반적이다.


하지만, Edit.c의 해당 부분 코드는 아래와 같다.


//////////////////
// 인코딩
//////////////////
cchTextW = MultiByteToWideChar(cpEdit,0,pszText,iSelCount,pszTextW,(int)LocalSize(pszTextW)/sizeof(WCHAR));
//(중략)
cchEscapedW = (int)LocalSize(pszEscapedW) / sizeof(WCHAR);
UrlEscape(pszTextW,pszEscapedW,&cchEscapedW,URL_ESCAPE_SEGMENT_ONLY);

cchEscaped = WideCharToMultiByte(cpEdit,0,pszEscapedW,cchEscapedW,pszEscaped,(int)LocalSize(pszEscaped),NULL,NULL);


//////////////////
// 디코딩
//////////////////
cchTextW = MultiByteToWideChar(cpEdit,0,pszText,iSelCount,pszTextW,(int)LocalSize(pszTextW)/sizeof(WCHAR));
//(중략)
cchUnescapedW = (int)LocalSize(pszUnescapedW) / sizeof(WCHAR);
UrlUnescape(pszTextW,pszUnescapedW,&cchUnescapedW,0);

cchUnescaped = WideCharToMultiByte(cpEdit,0,pszUnescapedW,cchUnescapedW,pszUnescaped,(int)LocalSize(pszUnescaped),NULL,NULL);


즉, UTF-8 변환을 아예 하지 않는다.

따라서 URL Decode UTF-8로 인코딩된 데이터를 그대로 유니코드 문자인 셈치고 읽는 것이다…



2. 첫번째 시도


이 기능을 정상적[각주:1]으로 동작하게 하려면 중간에 UTF-8 변환 부분을 추가해야 한다.

하지만, 생각해보니 이게 쉽지만은 않다.

UTF-8을 거쳐 UrlEscape된 데이터는 ASCII 텍스트인데, 메모장2에서 쓰려면 유니코드로 변환하는 과정이 추가로 필요하다.


이런 부분을 모두 고려한 코드를 일단 작성해봤다.


//////////////////
// 인코딩 추가
//////////////////

LPWSTR Unicode2UTF8W(LPWSTR pszUnicode, DWORD lenUnicode, DWORD *lenUTF8) {
    LPWSTR pszUTF8;
    register long lB = 0;
    register DWORD l;

    pszUTF8 = LocalAlloc(LPTR, (lenUnicode * 3 + 1) * sizeof(WCHAR));
    if (!pszUTF8) return NULL;

    for (l = 0; l < lenUnicode; l++) {
        if ((pszUnicode[l] & 0xff80) == 0) pszUTF8[lB++] = pszUnicode[l];
        else if ((pszUnicode[l] & 0xf800) == 0) {
            pszUTF8[lB++] = (pszUnicode[l] >> 6) & 0x1f | 0xc0;
            pszUTF8[lB++] = (pszUnicode[l]) & 0x3f | 0x80;
        }
        else {
            pszUTF8[lB++] = (pszUnicode[l] >> 12) & 0x0f | 0xe0;
            pszUTF8[lB++] = (pszUnicode[l] >> 6) & 0x3f | 0x80;
            pszUTF8[lB++] = (pszUnicode[l]) & 0x3f | 0x80;
        }

        if (!(pszUnicode[l])) break;
    }

    *lenUTF8 = lB;

    return pszUTF8;
}


//////////////////
// 디코딩 추가
//////////////////

BOOL IsUTF8W(LPWSTR pTest, int nLength)
{
    static int byte_class_table[256] = {
        /*       00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  */
        /* 00 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 10 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 20 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 30 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 40 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 50 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 60 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 70 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 80 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        /* 90 */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        /* A0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
        /* B0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
        /* C0 */ 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
        /* D0 */ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
        /* E0 */ 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 7,
        /* F0 */ 9,10,10,10,11, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
        /*       00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  */ };

    /* state table */
    typedef enum {
        kSTART = 0, kA, kB, kC, kD, kE, kF, kG, kERROR, kNumOfStates
    } utf8_state;

    static utf8_state state_table[] = {
        /*                            kSTART, kA,     kB,     kC,     kD,     kE,     kF,     kG,     kERROR */
        /* 0x00-0x7F: 0            */ kSTART, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0x80-0x8F: 1            */ kERROR, kSTART, kA,     kERROR, kA,     kB,     kERROR, kB,     kERROR,
        /* 0x90-0x9f: 2            */ kERROR, kSTART, kA,     kERROR, kA,     kB,     kB,     kERROR, kERROR,
        /* 0xa0-0xbf: 3            */ kERROR, kSTART, kA,     kA,     kERROR, kB,     kB,     kERROR, kERROR,
        /* 0xc0-0xc1, 0xf5-0xff: 4 */ kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0xc2-0xdf: 5            */ kA,     kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0xe0: 6                 */ kC,     kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0xe1-0xec, 0xee-0xef: 7 */ kB,     kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0xed: 8                 */ kD,     kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0xf0: 9                 */ kF,     kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0xf1-0xf3: 10           */ kE,     kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR,
        /* 0xf4: 11                */ kG,     kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR, kERROR };

#define BYTE_CLASS(b) (byte_class_table[(unsigned char)b])
#define NEXT_STATE(b,cur) (state_table[(BYTE_CLASS(b) * kNumOfStates) + (cur)])

    utf8_state current = kSTART;
    register int i;

    const WCHAR* pt = pTest;
    int len = nLength;

    for (i = 0; i < len; i++, pt++) {
        if ((*pt) & (~0xff)) return FALSE;
        current = NEXT_STATE(*pt, current);
        if (kERROR == current)
            break;
    }

    return (current == kSTART) ? TRUE : FALSE;
}

LPWSTR UTF82UnicodeW(LPWSTR pszUTF8, DWORD lenUTF8, DWORD *lenUnicode) {
    LPWSTR pszUnicode;
    register long l1 = 0, l2 = 0;

    pszUnicode = LocalAlloc(LPTR, (lenUTF8 + 1) * 3);

    while (pszUTF8[l1]) {
        if (!(pszUTF8[l1] & 0x80)) {
            pszUnicode[l2] = pszUTF8[l1];
            l1++;
        }
        else if ((pszUTF8[l1] & 0xe0) == 0xc0) {
            pszUnicode[l2] = ((pszUTF8[l1] & 0x1f) << 6) | (pszUTF8[l1 + 1] & 0x3f);
            l1 += 2;
        }
        else if ((pszUTF8[l1] & 0xf0) == 0xe0) {
            pszUnicode[l2] = ((pszUTF8[l1] & 0x0f) << 12) | ((pszUTF8[l1 + 1] & 0x3f) << 6) | (pszUTF8[l1 + 2] & 0x3f);
            l1 += 3;
        }
        else {
            l1++;
        }
        l2++;
    }
    pszUnicode[l2] = L'\0';
    *lenUnicode = l2;

    return pszUnicode;
}



3. 원작자(Florian Balmer)와 토의


메모장2 mod 프로젝트XhmikosR가 관리하는 프로젝트지만, 이 친구는 뭘 얘기해도 답이 없는 친구라 패스하고…

원작자인 Florian Balmer에게 이 코드를 보냈다.


그리고, 기대했던 대로 친절한 답변을 받았다.



메모장2의 ToDo 목록에 추가하겠다는 점이 가장 눈에 띄었다.


또한, 유추할 수 있는 내용은 이 쪽에선 URL Encode/Decode 기능 자체를 이해를 하지 못한다는 것.

아마도 필요 자체가 거의 없는 환경[각주:2]이라 그런 것 같다.


더불어, UrlEscapeA, MultiByteToWideChar 등의 함수를 사용하는 것이 더 나을 것 같다[각주:3]는 조언도 해줬다.



4-1. 두번째 시도: 인코딩


Balmer 씨의 의견을 적극 반영하여 OS에서 제공하는 라이브러리를 적극 사용하는 버전을 만들었다.


BOOL UrlUTF8Escape(LPWSTR pszUnicode, DWORD lenUnicode, UINT codePage, LPWSTR pszEscapedW, DWORD *cchEscapedW, DWORD dwFlags) {
    unsigned char *pszTemp1, *pszTemp2;
    DWORD lenTemp1 = lenUnicode * 3 + 1, lenTemp2 = lenUnicode * 3 * 3 + 1;
    BOOL ret = FALSE;

    pszTemp1 = LocalAlloc(LPTR, lenTemp1);
    pszTemp2 = LocalAlloc(LPTR, lenTemp2);

    if (pszTemp1 && pszTemp2) {
        lenTemp1 = WideCharToMultiByte(CP_UTF8, 0, pszUnicode, lenUnicode, pszTemp1, lenTemp1, NULL, NULL);
        if (lenTemp1) {
            // 실제로는 UrlEscapeA()가 기대한대로 동작하지 않음
            // 0x80 이상의 문자는 죄다 '?'(0x3F)로 처리함
            UrlEscapeA(pszTemp1, pszTemp2, &lenTemp2, dwFlags);
            (*cchEscapedW) = MultiByteToWideChar(codePage, 0, pszTemp2, lenTemp2, pszEscapedW, *cchEscapedW);
            ret = TRUE;
        }
    }

    if (pszTemp1) LocalFree(pszTemp1);
    if (pszTemp2) LocalFree(pszTemp2);
    return ret;
}


그런데, 막상 만들고 보니 정상적으로 동작하지 않는다.

면밀히 검토해보니, UrlEscapeA() 함수의 동작방식이 내가 기대한 것과 달랐다.

기본적으로 7비트 ASCII 문자에서만 동작하는 것이다.

즉, UTF-8로 변환한 문자열을 처리할 수 없다…


이러한 점을 고려해서 아래와 같이 수정했다.


BOOL UrlUTF8Escape(LPWSTR pszUnicode, DWORD lenUnicode, LPWSTR pszEscapedW, DWORD *cchEscapedW, DWORD dwFlags) {
    unsigned char *pszTemp1, *pszTemp2;
    WCHAR *pszTempW;
    int lenTemp1 = lenUnicode * 3 + 1, lenTemp2 = lenUnicode * 3 * 3 + 1;
    BOOL ret = FALSE;
    register int i;

    pszTemp1 = LocalAlloc(LPTR, lenTemp1);
    pszTempW = LocalAlloc(LPTR, lenTemp1 * sizeof(WCHAR));
    pszTemp2 = LocalAlloc(LPTR, lenTemp2);

    if (pszTemp1 && pszTempW && pszTemp2) {
        lenTemp1 = WideCharToMultiByte(CP_UTF8, 0, pszUnicode, lenUnicode, pszTemp1, lenTemp1, NULL, NULL);
        if (lenTemp1) {
            for (i = 0; i < lenTemp1; i++) {
                pszTempW[i] = (WCHAR)pszTemp1[i];
            }
            pszTempW[lenTemp1] = L'\0';

            // UrlEscapeW()는 UrlEscapeA()와 달리 0x80 이상의 문자도 정상적으로 변환함
            // UrlEscapeA()는 기대한대로 동작하지 않고, 0x80 이상의 문자를 '?'(0x3F)로 변환함
            UrlEscape(pszTempW, pszEscapedW, cchEscapedW, dwFlags);
            ret = TRUE;
        }
    }

    if (pszTemp1) LocalFree(pszTemp1);
    if (pszTempW) LocalFree(pszTempW);
    if (pszTemp2) LocalFree(pszTemp2);
    return ret;
}



4-2. 두번째 시도: 디코딩


디코딩 쪽은 인코딩보다는 깔끔하다.

UrlUnescapeA는 UrlEsacpeA와 달리 0x80 이상의 문자에 대해 특별한 문제를 야기하지 않는다.


BOOL UrlUTF8Unescape(LPWSTR pszUTF8, DWORD lenUTF8, UINT codePage, LPWSTR pszUnescapedW, DWORD *cchUnescapedW) {
    unsigned char *pszTemp1, *pszTemp2;
    int lenTemp1 = lenUTF8 * 3 + 1, lenTemp2 = lenUTF8 * 3 + 1;
    BOOL ret = FALSE;

    pszTemp1 = LocalAlloc(LPTR, lenTemp1);
    pszTemp2 = LocalAlloc(LPTR, lenTemp2);

    if (pszTemp1 && pszTemp2) {
        lenTemp1 = WideCharToMultiByte(codePage, 0, pszUTF8, lenUTF8, pszTemp1, lenTemp1, NULL, NULL);
        if (lenTemp1) {
            UrlUnescapeA(pszTemp1, pszTemp2, &lenTemp2, 0);
            if (lenTemp2 && IsUTF8(pszTemp2, lenTemp2)) {
                (*cchUnescapedW) = MultiByteToWideChar(CP_UTF8, 0, pszTemp2, lenTemp2, pszUnescapedW, *cchUnescapedW);
                ret = TRUE;
            }
        }
    }

    if (pszTemp1) LocalFree(pszTemp1);
    if (pszTemp2) LocalFree(pszTemp2);
    return ret;
}



5. 결론


이 내용이 모두 반영된 메모장2 mod별도 포스팅으로 공개했다.



  1. 정확히 말해, 우리가 흔히 접하는 환경과 동일하게 동작하게 하려면 [본문으로]
  2. Florian은 스위스인, XhmikosR은 그리스인 [본문으로]
  3. 위의 소스는 기본적으로 관련된 변환을 직접 코딩했음 [본문으로]
신고
  1. qp 2016.09.18 17:25 신고

    읽어봐도 모를거 같았지만, 혹시나 해서 읽어봤는데 역시나 몰라서
    그냥 공감 하나 찍고 다운받으러 갑니다 ㅌㅌ
    문돌이라서 전혀 다른 용도로 쓰고 있지만, 이거 없으면 불편해서 일 못해요 ㅎ

+ Recent posts