1. 발단
모든 일에는 시작이 있는 법…
발단은 메모장2 mod 포스팅에 달린 댓글 하나였다.
애초에 이 기능을 제대로 써볼 생각도 없었던지라 생각도 못했는데, 소스를 읽다보니 뭔가 많이 이상하다.
이 기능은 기본적으로 유니코드 → UTF-8 → UrlEscape 순으로 변환하는 게 일반적이다.
하지만, 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는 별도 포스팅으로 공개했다.
드디어 해결한 Notepad2-mod의 드래그앤드롭 오류 (2) | 2016.11.08 |
---|---|
Notepad2-mod r986 (5393ab8) #4 한국어화 버전 공개 (1) | 2016.10.24 |
Notepad2-mod r986 (5393ab8) #3 한국어화 버전 공개 (3) | 2016.09.14 |
Notepad2-mod r986 (5393ab8) #1 한국어화 버전 공개 (2) | 2016.09.11 |
Notepad2-mod r981 (8711668) #2 한국어화 버전 공개 (22) | 2016.07.25 |