C++ / Base64 Encoder
(UTF-8 base64 encoding )


Intro

 C++ 에서 base64 로 encoding 을 하는것은 어렵지 않습니다.

왜냐면 base64 encoding 자체가 간단하기 때문입니다.


아시다시피 base64 인코딩은, 문자열(혹은 메모리값)을 8bit 가 아닌 6bit 단위로 끊고, 이를 character map 에 해당하는 값으로 치환하면 됩니다.

오히려 이런 bit 단위 처리에서는 java, .NET 보다는 C/C++ 이 더 편합니다.

그런데 C++ 에서는 문제가 되는것이 있었으니, 바로 character set 입니다.

EUC-KR, UTF-8, UTF-16 같은 것들이죠.

그리고 멀티바이트니 유니코드니, 복잡합니다.


이런 이유로 java, .NET, Web(javascript)에서 encoding 한 값과 C++ 에서 인코딩한 값이 다르기 일쑤입니다.



Intent

java, .NET, Web 처럼 C++ 에서도 한글/일본어 같은 3byte 문자도 base64 인코딩을 정상적으로(?) 할 수 있도록 해 봅시다.


먼저 다음은 Java 에서 "대한민국" 이라는 문자열을 base64로 인코딩한 코드입니다.



import java.io.UnsupportedEncodingException;

import org.apache.commons.codec.binary.Base64;

public class EncodeTest {
	public static void main(String args[]) throws UnsupportedEncodingException {
		String plain = "대한민국";
		System.out.println(Base64.encodeBase64URLSafeString(plain.getBytes("utf-8")));
	}
}


결과는 다음과 같네요.



이번에는 웹에서 해 보도록 하죠.

http://www.opinionatedgeek.com/dotnet/tools/base64encode/ 


결과는 아래와 같습니다.




차이점이 보이시나요?

'-' 와 '+' 입니다.

이 부분은 base64 encoding 의 옵션에 해당하는 부분인데, 이부분에 대해서는 wikipedia.org 를 참고하시기 바랍니다.

('=' 에 대한 부분도 읽어보세요)


아무튼 결과는 동일하게 나왔습니다.

이젠 C++ 에서 해 보도록 하죠.



//===========================================================
// base64
//===========================================================
std::wstring base64Encode(const wstring input){
	//string utf8_input = wstrToUtf8(input);

	unsigned char const* buffer = (unsigned const char*)(input.c_str());
	size_t size = input.length();

    using std::wstring;
    static wchar_t const* base64Table =
        L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    
	// for =
    // size_t base64Size = (size + 2 - ((size + 2) % 3)) / 3 * 4;
	// wstring result(base64Size, L'=');

	size_t base64Size = ceil(size * 4.0 / 3);
    wstring result(base64Size, L'');

    unsigned char const* s = buffer;  // source pointer
    size_t di = 0;                    // destination index
    for(size_t i = 0; i < size / 3; i++){
        // input: 0000_0000 0000_0000 0000_0000
        // 
        // out1:  0000_00
        // out2:         00 0000
        // out3:                _0000 00
        // out4:                        00_0000
        
        result[di++] = base64Table[s[0] >> 2];
        result[di++] = base64Table[((s[0] << 4) | (s[1] >> 4)) & 0x3f];
        result[di++] = base64Table[((s[1] << 2) | (s[2] >> 6)) & 0x3f];
        result[di++] = base64Table[s[2] & 0x3f];
        s += 3;
    }

    size_t remainSize = size % 3;
    switch(remainSize){
    case 0:
        break;
    case 1:
        result[di++] = base64Table[s[0] >> 2];
        result[di++] = base64Table[(s[0] << 4) & 0x3f];
        break;
    case 2:
        result[di++] = base64Table[s[0] >> 2];
        result[di++] = base64Table[((s[0] << 4) | (s[1] >> 4)) & 0x3f];
        result[di++] = base64Table[(s[1] << 2) & 0x3f];
        break;
    default:
        throw std::logic_error("Should not happen.");
    }
    return result;
}

int main() {
	wcout.imbue(locale("korean"));

	wstring plain(L"대한민국");
	wcout << base64Encode(plain).c_str() << endl;

	return 0;
}






결과는 아래와 같습니다.



C++ 에서는 Unicode 관련 문제로 인하여 결과값이 다르게 나옵니다.

참고로 위 코드는 '문자집합' 을 '유니코드 문자 집합 사용' 으로 한 프로젝트입니다.




Use Case

 원인은 유니코드니, 멀티바이트니, UTF-8 이니 하는 것들이 문제입니다.

실제로 Visual Studio 에서 wstring::c_str() 을 통해 가져온 문자열의 메모리 값을 살펴보면,

한글이 2byte 로 처리되어 있는 것을 볼 수 있습니다.


결국 base64 로 인코딩 하기 전에 wstring 의 값을 utf-8 로 변환해 주어야 합니다.

아래는 정상적으로 동작하는 C++ 코드입니다.


차이점은 코드로 확인하시기 바랍니다.



void wstrToUtf8(string& dest, const wstring& src){
	dest.clear();
	for (size_t i = 0; i < src.size(); i++){
		wchar_t w = src[i];
		if (w <= 0x7f)
			dest.push_back((char)w);
		else if (w <= 0x7ff){
			dest.push_back(0xc0 | ((w >> 6)& 0x1f));
			dest.push_back(0x80| (w & 0x3f));
		}
		else if (w <= 0xffff){
			dest.push_back(0xe0 | ((w >> 12)& 0x0f));
			dest.push_back(0x80| ((w >> 6) & 0x3f));
			dest.push_back(0x80| (w & 0x3f));
		}
		else if (w <= 0x10ffff){
			dest.push_back(0xf0 | ((w >> 18)& 0x07));
			dest.push_back(0x80| ((w >> 12) & 0x3f));
			dest.push_back(0x80| ((w >> 6) & 0x3f));
			dest.push_back(0x80| (w & 0x3f));
		}
		else
			dest.push_back('?');
	}
}


string wstrToUtf8(const wstring& str){
	string result;
	wstrToUtf8(result, str);
	return result;
}


//===========================================================
// base64
//===========================================================
std::wstring base64Encode(const wstring input){
	string utf8_input = wstrToUtf8(input);

	unsigned char const* buffer = (unsigned const char*)(utf8_input.c_str());
	size_t size = utf8_input.length();

    using std::wstring;
    static wchar_t const* base64Table =
        L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    
	// for =
    // size_t base64Size = (size + 2 - ((size + 2) % 3)) / 3 * 4;
	// wstring result(base64Size, L'=');

	size_t base64Size = ceil(size * 4.0 / 3);
    wstring result(base64Size, L'');

    unsigned char const* s = buffer;  // source pointer
    size_t di = 0;                    // destination index
    for(size_t i = 0; i < size / 3; i++){
        // input: 0000_0000 0000_0000 0000_0000
        // 
        // out1:  0000_00
        // out2:         00 0000
        // out3:                _0000 00
        // out4:                        00_0000
        
        result[di++] = base64Table[s[0] >> 2];
        result[di++] = base64Table[((s[0] << 4) | (s[1] >> 4)) & 0x3f];
        result[di++] = base64Table[((s[1] << 2) | (s[2] >> 6)) & 0x3f];
        result[di++] = base64Table[s[2] & 0x3f];
        s += 3;
    }

    size_t remainSize = size % 3;
    switch(remainSize){
    case 0:
        break;
    case 1:
        result[di++] = base64Table[s[0] >> 2];
        result[di++] = base64Table[(s[0] << 4) & 0x3f];
        break;
    case 2:
        result[di++] = base64Table[s[0] >> 2];
        result[di++] = base64Table[((s[0] << 4) | (s[1] >> 4)) & 0x3f];
        result[di++] = base64Table[(s[1] << 2) & 0x3f];
        break;
    default:
        throw std::logic_error("Should not happen.");
    }
    return result;
}



int main() {
	wcout.imbue(locale("korean"));

	wstring plain(L"대한민국");
	wcout << base64Encode(plain).c_str() << endl;

	return 0;
}




결과는..





Result


문자열 자릿수에 따라 = 로 buff 가 채워지는 방식이 있고, 위 코드처럼 별도로 채우지 않는 방식이 있습니다.

또한 +를 - 로 치환한다거나(web에서 사용하기 위해서), 일정 크기길이가 되면 CR/LF 를 먹이는 방식도 있죠.

이런 각종 옵션들은 base64 문서를 참고하시기 바랍니다.


규칙을 이해하고 나시면, 코드에 적용하는건 쉽게 하실거라고 생각합니다.

마지막으로 Decoding 은 위 코드를 참고하여, 만들어 보시기 바랍니다. :)