TEUS.me

 
 

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. 위의 소스는 기본적으로 관련된 변환을 직접 코딩했음 [본문으로]
TAG :

이 글을 공유합시다

facebook twitter kakaoTalk kakaostory naver band

본문과 관련 있는 내용으로 댓글을 남겨주시면 감사하겠습니다.

비밀글모드

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